1月28日 C Primer Plus学习

10.4 函数、数组和指针

既然能使用指针表示数组名,也可以用数组名表示指针。

只有在函数原型或函数定义头中,才可以用int ar[]代替int * ar:

int sum (int ar[], int n);

int *ar形式和int ar[]形式都表示ar是一个指向int的指针。但是,int ar[]只能用于声明形式参数。第2种形式(int ar[])提醒读者指针ar指向的不仅仅一个int类型值,还是一个int类型数组的元素。

声明数组形参因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。只有在这种情况下,C才会把int ar[]和int * ar解释成一样。也就是说,ar是指向int的指针。由于函数原型可以省略参数名,所以下面4种原型都是等价的:

int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);

但是,在函数定义中不能省略参数名。下面两种形式的函数定义等价:

int sum(int *ar, int n)
{
	// 其他代码已省略
}
int sum(int ar[], int n);
{
	//其他代码已省略
}
10.4.1 使用指针形参

函数要处理数组必须知道何时开始、何时结束。sum()函数使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。但是这并不是给函数传递必备信息的唯一方法。还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同),第2个指针指明数组的结束处。程序清单10.11演示了这种方法,同时该程序也表明了指针形参是变量,这意味着可以用索引表明访问数组中的哪一个元素。

程序清单10.11 sum_arr2.c程序

/* sum_arr2.c -- 数组元素之和 */
#include<stdio.h>
#define SIZE 10
int sump(int * start, int * end);
int main(void)
{
	int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };
    long answer;
    answer = sump(marbles, marbles + SIZE);
    printf("The total number of marbles is %ld.\n", answer);
    return 0;
}
/* 使用指针算法 */
int sump(int * start, int * end)
{
	int total = 0;
	while (start < end)
	{
		total += *start;  // 把数组元素的值加起来
		start++;      // 让指针指向下一个元素
	}
	return total;
}

因为while循环的测试条件是一个不相等的关系,所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。这使得 while循环的测试条件是有效的,因为 start在循环中最后的值是end[1]。注意,使用这种“越界”指针的函数调用更为简洁:

answer = sump(marbles, marbles + SIZE);

因为下标从0开始,所以marbles + SIZE指向数组末尾的下一个位置。如果end指向数组的最后一个元素而不是数组末尾的下一个位置,则必须使用下面的代码:

answer = sump(marbles, marbles + SIZE - 1);

这种写法既不简洁也不好记,很容易导致编程错误。顺带一提,虽然C 保证了marbles + SIZE有效,但是对marbles[SIZE](即储存在该位置上的值)未作任何保证,所以程序不能访问该位置。

还可以把循环体压缩成一行代码:

total += *start++;

一元运算符和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是start。也就是说,指针start先递增后指向。使用后缀形式

(即start++而不是++start)意味着先把指针指向位置上的值加到total上,然后再递增指针。如果使用++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用(start)++,则先使用start指向的值,再递增该值,而不是递增指针。这样,指针将一直指向同一个位置,但是该位置上的值发生了变化。虽然start++的写法比较常用,但是(start++)这样写更清楚。程序清单10.12的程序演示了这些优先级的情况。

10.4.2 指针表示法和数组表示法

处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法。

至于C语言,ar[i]和*(ar+1)这两个表达式都是等价的。无论ar是数组名还是指针变量,这两个表达式都没问题。但是,只有当ar是指针变量时,才能使用ar++这样的表达式。

指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器在编译时能生成效率更高的代码。然而,许多程序员认为他们的主要任务是确保代码正确、逻辑清晰,而代码优化应该留给编译器去做。

10.5 指针操作

程序清单10.13 ptr_ops.c程序

// ptr_ops.c -- 指针操作 
#include<stdio.h> 
int main(void)
{
    int urn[5] = { 100, 200, 300, 400, 500 };
	int * ptr1, *ptr2, *ptr3;
	ptr1 = urn;         // 把一个地址赋给指针 
    ptr2 = &urn[2];     // 把一个地址赋给指针
    					// 解引用指针,以及获得指针的地址
	printf("pointer value, dereferenced pointer, pointer address:\n");
    printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
    
    // 指针加法
	ptr3 = ptr1 + 4;
	printf("\nadding an int to a pointer:\n");
    printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
	ptr1++;         // 递增指针
	printf("\nvalues after ptr1++:\n"); printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
	ptr2--;         // 递减指针
	printf("\nvalues after --ptr2:\n"); printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
	--ptr1;         // 恢复为初始值 
    ++ptr2;         // 恢复为初始值
	printf("\nPointers reset to original values:\n");
    printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
	// 一个指针减去另一个指针
	printf("\nsubtracting one pointer from another:\n");
	printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);
	// 一个指针减去一个整数
	printf("\nsubtracting an int from a pointer:\n");
	printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
    
    return 0;
}

输出:

pointer value, dereferenced pointer, pointer address:

ptr1 = 0x7fff5fbff8d0, *ptr1 =100, &ptr1 = 0x7fff5fbff8c8 adding an int to a pointer:

ptr1 + 4 = 0x7fff5fbff8e0, *(ptr1 + 4) = 500 values after ptr1++:

ptr1 = 0x7fff5fbff8d4, *ptr1 =200, &ptr1 = 0x7fff5fbff8c8 values after --ptr2:

ptr2 = 0x7fff5fbff8d4, *ptr2 = 200, &ptr2 = 0x7fff5fbff8c0 Pointers reset to original values:

ptr1 = 0x7fff5fbff8d0, ptr2 = 0x7fff5fbff8d8 subtracting one pointer from another:

ptr2 = 0x7fff5fbff8d8, ptr1 = 0x7fff5fbff8d0, ptr2 - ptr1 = 2 subtracting an int from a pointer:

ptr3 = 0x7fff5fbff8e0, ptr3 - 2 = 0x7fff5fbff8d8

赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。

地址应该和指针类型兼容。也就是说,不能把 double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。

解引用:运算符给出指针指向地址上储存的值。因此,ptr1的初值是100,该值储存在编号为0x7fff5fbff8d0的地址上。

取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。本例中,ptr1 储存在内存编号为 0x7fff5fbff8c8 的地址上,该存储单元储存的内容是0x7fff5fbff8d0,即urn的地址。因此&ptr1是指向ptr1的指针,而ptr1是指向utn[0]的指针。

指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。因此ptr1 +4与&urn[4]等价。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节), ptr1指向urn[1]。现在ptr1的值是 0x7fff5fbff8d4(数组的下一个元素的地址),*ptr的值为200(即urn[1]的

值)。注意,ptr1本身的地址仍是 0x7fff5fbff8c8。毕竟,变量不会因为值发生变化就移动位置。

图10.4 递增指向int的指针

指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第 2 个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3 - 2与 &urn[2]等价,因为ptr3指向的是&arn[4]。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。

递减指针:当然,除了递增指针还可以递减指针。在本例中,递减ptr3 使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算符都可以使用。注意,在重置ptr1和ptr2前,它们都指向相同的元素urn[1]。

指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如,程序清单10.13的输出中,ptr2 - ptr1得2,意思是这两个指针所指向的两个元素相隔两个int,而不是2字节。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第 1 个地址),C 都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。

比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。

注意,这里的减法有两种。可以用一个指针减去另一个指针得到一个整数,或者用一个指针减去一个整数得到另一个指针。

在递增或递减指针时还要注意一些问题。编译器不会检查指针是否仍指向数组元素。C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效。但是,如果递增或递减一个指针后超出了这个范围,则是未定义的。另外,可以解引用指向数组任意元素的指针。但是,即使指针指向数组后面一个位置是有效的,也能解引用这样的越界指针。

编程练习第8题

代码
点击查看
#include<stdio.h>
void copy_ptr(double *target, double *source, int n)
{
	for (int i = 0; i < n; i++)
		*(target+i) = *(source+i);
}
void main(void)
{
	double source[7] = {48, 65, 15, 68, 99, 51, 37};
	double target[3];
	copy_ptr(target, source + 2, 3);
	for (int i = 0; i < 3; i++)
		printf("%.0lf  ", target[i]);
	return;
}
运行结果

编程练习第9题

代码
点击查看
#include<stdio.h>
void copyArray(int n, int m, double target[n][m], double source[n][m])
{
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			target[i][j] = source[i][j];
	return;
}
void showArray(int n, int m, double target[n][m])
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
			printf("%.0lf  ", target[i][j]);
		printf("\n");
	}
	return;
}
void main(void)
{
	int n = 3, m = 5;
	double target[n][m], source[][5] = 
	{
		{78, 91, 54, 87, 83},
		{51, 43, 97, 56, 47},
		{95, 94, 67, 12, 32},
	};
	copyArray(n, m, target, source);
	showArray(n, m, target);
	return;
}
运行结果

posted @ 2022-02-07 11:17  shucharjer  阅读(114)  评论(0)    收藏  举报