预计阅读本页时间:-
假设您要编写一个对数组进行操作的函数,目的是要此函数返回数组内所有元素的和,并假设marbles为这个int数组的名称。应该如何调用这个函数?一种合乎情理的猜测如下:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
那么原型应该是什么样的?数组名同时代表数组首元素的地址,因此实际参数marbles是一个int的地址,应把它赋给一个类型为指向int的指针的形式参量:
函数sum()从该参数可以得到什么信息呢?它得到数组首元素的地址,而且知道可以从此地址找到一个int。请注意它无从知道数组中元素的数量。于是在函数的定义中有两种选择,第一种是在函数代码中写上固定的数组大小,如下所示:
上面的代码利用了这样的事实:正如可以在指针符号中使用数组名一样,也可以在数组符号中使用指针。同时,运算符+=把其右边的操作数加到左边。因此,total得到的是数组元素的和。
这种函数定义是有限制的,它仅在数组大小为10时可以工作。更灵活的方法是把数组大小做为第二个参数传递给函数。
这里的第一个参数把数组地址和数组类型的信息传递给函数,第二个参数把数组中的元素个数传递给函数。此外,关于函数参量还有一件需要说明的事情:在函数原型或函数定义头的场合中(并且也只有在这两种场合中),可以用int *ar代替int ar[]:
无论在任何情况下,形式int *ar都表示ar是指向int的指针。形式int ar[]也可以表示ar是指向int的指针,但只是在声明形式参量时才可以这样使用。使用第二种形式可以提醒读者ar不仅指向一个int数值,而且它指向的这个int是一个数组中的元素。
声明数组参量
由于数组名就是数组首元素的地址,所以如果实际参数是一个数组名,那么形式参量必须是与之相匹配的指针。在(而且仅在)这种场合中,C对于int ar[]和int *ar作出同样解释,即ar是指向int的指针。由于原型允许省略名称,因此下面的4种原型都是等价的:
定义函数时,名称是不可以省略的。因此,在定义时下面两种形式是等价的:
前面提到的4种原型是通用的,它们的函数定义可以采用上面两者之一。这些形式您都应该掌握。
程序程序清单10.10是一个使用函数sum()的程序。为了说明关于数组参数的一个有趣的事实,此程序同时打印出原数组的大小和代表数组的函数参量的大小(如果您的编译器不支持用%zd说明符打印sizeof的返回值,请使用%u或者%lu)。
清单10.10 sum_arr1.c程序
在我们的系统上输出结果如下:
请注意marbles的大小为40字节。的确如此,因为marbles包含10个int类型的数,每个数占4个字节,因此总共占用40个字节。但是ar的大小只有4个字节。这是因为ar本身并不是一个数组,它是一个指向marbles的首元素的指针。对于采用4字节地址的计算机系统,指针的大小为4个字节(其他系统中地址大小可能不是4个字节)。总之,在程序清单10.10中,marbles是一个数组,而ar为一个指向marbles首元素的指针,C中数组和指针之间的关系允许您在数组符号中使用指针ar。
10.4.1 使用指针参数
使用数组的函数需要知道何时开始和何时结束数组。函数sum()使用一个指针参量来确定数组的开始点,使用一个整数参量来指明数组的元素个数(指针参量同时确定了数组中数据的类型)。但是这并不是向函数传递数组信息的惟一方法。另一种方法是传递两个指针,第一个指针指明数组的起始地址(同前面的方法相同),第二个指针指明数组的结束地址。程序清单10.11中的示例程序示意了这种方法。这个例子同时利用了指针参数是变量这一事实,也就是说,程序中没有使用索引来指示数组中的每个元素,而是直接修改指针本身,使指针依次指向各个数组元素。程序清单10.11示范了这种技巧的使用。
程序清单10.11 sum_arr2.c程序
由于指针start最初指向marbles的首元素,因此执行赋值表达式total+=*start时,把首元素的值(即20)加到total上。然后表达式start++使指针变量start增1,从而指向数组的下一个元素。start是指向int的指针,因此当start增1时它将增加1个int的大小。
请注意函数sump()和sum()结束加法循环的方式不一样。函数sum()使用数组元素的个数做为第二个参数,循环利用这个值来控制循环次数:
而函数sump()则使用第二个指针来控制循环次数:
因为这是一个对于不相等关系的判断,所以处理的最后一个元素将是end所指向的位置之前的元素。这就意味着end实际指向的位置是在数组最后一个元素之后。C保证在为数组分配存储空间的时候,指向数组之后的第一个位置的指针也是合法的。这使上面例子中采用的结构是有效的,因为start在循环中最后得到的值是end。请注意使用这种“越界”指针可使函数调用的形式更整洁:
由于索引是从0开始的,因此marbles+SIZE指向数组结尾处之后的下一个元素。如果让end指向最后一个元素而不是指向数组结尾处之后的下一个元素,就需要使用下面的代码:
这种写法不仅仅看起来不整洁,而且也不容易被记住,因此比较容易导致编程错误。顺便说一句,尽管C保证指针marbles+SIZE是合法的,但对marbles[SIZE](即该地址存储的内容)不作任何保证。
可以把上面的循环体精简为一行代码:
一元运算符*和++具有相等的优先级,但它在结合时是从右向左进行的。这就意味着++应用于start,而不是应用于*start。也就是说,是指针自增1,而不是指针所指向的数据自增1。后缀形式(即start++,而不是++start)表示先把指针指向的数据加到total上,然后指针再自增1。如果程序使用*++start,则顺序就变为指针先自增1,然后再使用其指向的值。然而如果程序使用(*start)++,那么会使用start所指向的数据,然后再使该数据自增1,而不是使指针自增1。这样,指针所指向的地址不变,但其中的元素却变成了一个新数据。尽管*start++比较常用,但为了清晰起见,应该使用*(start++)。程序清单10.12中的程序示意了这些有关优先级的微妙之处:
程序清单10.12 order.c程序
输出结果如下:
上面例子中只有(*p3)++改变了数组元素的值。其他两个操作增加了指针p1和指针p2,使之指向下一个数组元素。
10.4.2 评论:指针和数组
从前面的介绍可以看出,处理数组的函数实际上是使用指针做为参数的。但是在编写处理数组的函数时,数组符号和指针符号都是可以选用的。如果使用数组符号(如程序清单10.10所示),则函数处理数组这一事实更加明显。同时,对于习惯于其他编程语言(如FORTRAN、Pascal、Modula-2或BASIC)的程序员来说,使用数组也更为熟悉。也有一些程序员可能更习惯于使用指针,觉得指针使用起来更加自然。程序清单10.11是使用指针的例子。
在C中,两个表达式ar[i]和*(ar+i)的意义是等价的。而且不管ar是一个数组名还是一个指针变量,这两个表达式都可以工作。然而只有当ar是一个指针变量时,才可以使用ar++这样的表达式。
指针符号(尤其是在对其使用增量运算符时)更接近于机器语言,而且某些编译器在编译时能够生成效率更高的代码。然而,很多程序员认为程序员的主要任务是保证程序的正确性和易读性,代码的优化应留给编译器去做。