C语言学习栏目目录
目录
源码 VS2019环境
假设要编写一个处理数组的函数,该函数返回数组中所有元素之和,待处理的是名为marbles的int类型数组。应该如何调用该函数?也许是下面这样:
total = sum(marbles); // 可能的函数调用
那么,该函数的原型是什么?记住,数组名是该数组首元素的地址,所以实际参数marbles是一个储存int类型值的地址,应把它赋给一个指针形式参数,即该形参是一个指向int的指针:
int sum(int * ar); // 对应的函数原型
sum()从该参数获得了什么信息?它获得了该数组首元素的地址,知道要在该位置上找出一个整数。注意,该参数并未包含数组元素个数的信息。我们有两种方法让函数获得这一信息。第一种方法是,在函数代码中写上固定的数组大小:
int sum(int * ar) // 相应的函数定义
{
int i;
int total = 0;
for (i = 0; i < 10; i++) // 假设数组有10个元素
total += ar[i]; // ar[i] 与 *(ar + i) 相同
return total;
}
既然能使用指针表示数组名,也可以用数组名表示指针。另外,回忆一下,+=运算符把右侧运算对象加到左侧运算对象上。因此,total是当前数组元素之和。
函数定义有限制,只能计算10个int类型的元素。另一个比较灵活的方法是把数组大小作为第2个参数:
int sum(int * ar, int n) // 更通用的方法
{
int i;
int total = 0;
for (i = 0; i < n; i++) // 使用 n 个元素
total += ar[i]; // ar[i] 和 *(ar + i) 相同
return total;
}
这里,第1个形参告诉函数该数组的地址和数据类型,第2个形参告诉函数该数组中元素的个数。
关于函数的形参,还有一点要注意。只有在函数原型或函数定义头中,才可以用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);
{
//其他代码已省略
}
可以使用以上提到的任意一种函数原型和函数定义。
下程序清单演示了一个程序,使用 sum()函数。该程序打印原始数组的大小和表示该数组的函数形参的大小(如果你的编译器不支持用转换说明%zd打印sizeof返回值,可以用%u或%lu来代替)。
/************************************************************************
功能: 数组元素之和
/************************************************************************/
// 如果编译器不支持 %zd,用 %u 或 %lu 替换它
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26,
31, 20 };
long answer;
answer = sum(marbles, SIZE);
printf("The total number of marbles is %ld.\n", answer);
printf("The size of marbles is %zd bytes.\n",
sizeof marbles);
return 0;
}
int sum(int ar[], int n) // 这个数组的大小是?
{
int i;
int total = 0;
for (i = 0; i < n; i++)
total += ar[i];
printf("The size of ar is %zd bytes.\n", sizeof ar);
return total;
}
该程序的输出如下:
The size of ar is 4 bytes.
The total number of marbles is 190.
The size of marbles is 40 bytes.
注意,marbles的大小是40字节。这没问题,因为marbles内含10个int类型的值,每个值占4字节,所以整个marbles的大小是40字节。但是,ar才4字节。这是因为ar并不是数组本身,它是一个指向 marbles 数组首元素的指针。简而言之,在上程序清单中,marbles是一个数组, ar是一个指向marbles数组首元素的指针,利用C中数组和指针的特殊关系,可以用数组表示法来表示指针ar。
1 使用指针形参
函数要处理数组必须知道何时开始、何时结束。sum()函数使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。但是这并不是给函数传递必备信息的唯一方法。还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同),第2个指针指明数组的结束处。下程序清单演示了这种方法,同时该程序也表明了指针形参是变量,这意味着可以用索引表明访问数组中的哪一个元素。
/************************************************************************
功能: 数组元素之和
/************************************************************************/
#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;
}
指针start开始指向marbles数组的首元素,所以赋值表达式total += *start把首元素(20)加给total。然后,表达式start++递增指针变量start,使其指向数组的下一个元素。因为start是指向int的指针,start递增1相当于其值递增int类型的大小。
注意,sump()函数用另一种方法结束加法循环。sum()函数把元素的个数作为第2个参数,并把该参数作为循环测试的一部分:
for( i = 0; i < n; i++)
而sump()函数则使用第2个指针来结束循环:
while (start < end)
因为while循环的测试条件是一个不相等的关系,所以循环最后处理的一个元素是end所指向位置的前一个元素。这意味着end指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。这使得 while循环的测试条件是有效的,因为 start在循环中最后的值是end(在最后一次while循环中执行完start++;后,start的值就是end的值)。注意,使用这种“越界”指针的函数调用更为简洁:
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++)这样写更清楚。程序清单下的程序演示了这些优先级的情况。
/************************************************************************
功能: 指针运算中的优先级
/************************************************************************/
#include <stdio.h>
int data[2] = { 100, 200 };
int moredata[2] = { 300, 400 };
int main(void)
{
int* p1, * p2, * p3;
p1 = p2 = data;
p3 = moredata;
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n", *p1++, *++p2, (*p3)++);
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
return 0;
}
下面是该程序的输出:
*p1 = 100, *p2 = 100, *p3 = 300
*p1++ = 100, *++p2 = 200, (*p3)++ = 300
*p1 = 200, *p2 = 200, *p3 = 301
只有(*p3)++改变了数组元素的值,其他两个操作分别把p1和p2指向数组的下一个元素。
2 指针表示法和数组表示法
从以上分析可知,处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法。如程序清单1所示,使用数组表示法,让函数是处理数组的这一意图更加明显。另外,许多其他语言的程序员对数组表示法更熟悉,如FORTRAN、Pascal、Modula-2或BASIC。其他程序员可能更习惯使用指针表示法,觉得使用指针更自然,如程序清单2所示。至于C语言,ar[i]和*(ar+1)这两个表达式都是等价的。无论ar是数组名还是指针变量,这两个表达式都没问题。但是,只有当ar是指针变量时,才能使用ar++这样的表达式。
指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器在编译时能生成效率更高的代码。然而,许多程序员认为他们的主要任务是确保代码正确、逻辑清晰,而代码优化应该留给编译器去做。