数组用于储存相同类型的数据。C把数组看作是派生类型,因为数组是建立在其他类型的基础上。也就是说,无法简单地声明一个数组。在声明数组时必须说明其元素的类型,如int类型的数组、float类型的数组,或其他类型的数组。所谓的其他类型也可以是数组类型,这种情况下,创建的是数组的数组(或称为二维数组)。
通常编写一个函数来处理数组,这样在特定的函数中解决特定的问题,有助于实现程序的模块化。在把数组名作为实际参数时,传递给函数的不是整个数组,而是数组的地址(因此,函数对应的形式参数是指针)。为了处理数组,函数必须知道从何处开始读取数据和要处理多少个数组元素。数组地址提供了“地址”,“元素个数”可以内置在函数中或作为单独的参数传递。第2种方法更普遍,因为这样做可以让同一个函数处理不同大小的数组。
对于传统的C数组,必须用常量表达式指明数组的大小,所以数组大小在编译时就已确定。C99/C11新增了变长数组,可以用变量表示数组大小。这意味着变长数组的大小延迟到程序运行时才确定。
一、指针和多维数组
指针和多维数组有什么关系?为什么要了解它们的关系?处理多维数组的函数要用到指针,所以在使用这种函数之前,先要更深入地学习指针。至于第1个问题,我们通过几个示例来回答。为简化讨论,我们使用较小的数组。假设有下面的声明:
// 内含int数组的数组
int zippo[4][2];
然后数组名zippo是该数组首元素的地址。在本例中,zippo的首元素是一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。下面,我们从指针的属性进一步分析。
因为zippo是数组首元素的地址,所以zippo
的值和&zippo[0]
的值相同。而zippo[0]
本身是一个内含两个整数的数组,所以zippo[0]
的值和它首元素(一个整数)的地址(即&zippo[0][0]
的值)相同。简而言之,zippo[0]
是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo和zippo[0]的值相同。
给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo
和zippo[0]
不同,因为zippo指向的对象占用了两个int大小,而zippo[0]
指向的对象只占用一个int大小。因此,zippo+1
和zippo[0]+1
的值不同。
解引用一个指针(在指针前使用*
运算符)或在数组名后使用带下标的[]
运算符,得到引用对象代表的值。因为zippo[0]
是该数组首元素(zippo[0][0]
)的地址,所以*(zippo[0])
表示储存在zippo[0][0]
上的值(即一个int类型的值)。与此类似,*zippo
代表该数组首元素(zippo[0]
)的值,但是zippo[0]
本身是一个int类型值的地址。该值的地址是&zippo[0][0]
,所以*zippo
就是&zippo[0][0]
。对两个表达式应用解引用运算符表明,**zippo
与*&
与*&zippo[0][0]
等价,这相当于zippo[0][0]
,即一个int类型的值。简而言之,zippo
是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(double indirection)的例子。
//
// 多维数组
//
#include <stdio.h>
int main(void)
{
int zippo[4][2] = {{1, 2}, {3, 4}, {5, 6}, {7, 8}};
printf("zippo=%p, zippo + 1=%p\n", zippo, zippo + 1);
printf("zippo[0]=%p, zippo[0] + 1=%p\n", zippo[0], zippo[0] + 1);
printf("*zippo=%p, *zippo + 1=%p\n", *zippo, *zippo + 1);
printf("zippo[0][0] = %d\n", zippo[0][0]);
printf("*zippo[0] = %d\n", *zippo[0]);
printf("**zippo = %d\n", **zippo);
printf("zippo[2][1] = %d\n", zippo[2][1]);
printf("*(*(zippo+2)+1) = %d\n", *(*(zippo + 2) + 1));
return 0;
}
zhgxun-pro:c2 zhgxun$ gcc test.c
zhgxun-pro:c2 zhgxun$ ./a.out
zippo=0x7fff5c9d9bd0, zippo + 1=0x7fff5c9d9bd8
zippo[0]=0x7fff5c9d9bd0, zippo[0] + 1=0x7fff5c9d9bd4
*zippo=0x7fff5c9d9bd0, *zippo + 1=0x7fff5c9d9bd4
zippo[0][0] = 1
*zippo[0] = 1
**zippo = 1
zippo[2][1] = 6
*(*(zippo+2)+1) = 6
zhgxun-pro:c2 zhgxun$
要特别注意,与zippo[2][1]
等价的指针表示法是*(*(zippo+2)+1)
。看上去比较复杂,应最好能理解。下面列出了理解该表达式的思路:
标识 | 描述 |
---|---|
zippo |
二维数组首元素的地址(每个元素都是内含两个int类型元素的一位数组) |
zippo+2 |
二维数组的第三个元素(即是一维数组)的地址 |
*(zippo+2) |
二维数组第三个元素(即是一维数组)的首元素(一个int类型的值)地址 |
*(zippo+2)+1 |
二维数组第三个元素(即是一维数组)的第二个元素(也是一个int类型的值)地址 |
*(*(aippo+2)+1) |
二维数组的第三个一维数组元素的第二个int类型元素的值,即数组的第三行第二列的值(zippo[2][1]) |
以上分析并不是为了说明用指针表示法(*(*(zippo+2)+1)
)代替数组表示法(zippo[2][1]
),而是提示读者,如果程序恰巧使用一个指向二维数组的指针,而且要通过该指针获取值时,最好用简单的数组表示法,而不是指针表示法。
二、指向多维数组的指针
如何声明一个指针变量pz指向一个二维数组(如,zippo)?在编写处理类似zippo这样的二维数组时会用到这样的指针。把指针声明为指向int的类型还不够。因为指向int只能与zippo[0]的类型匹配,说明该指针指向一个int类型的值。但是zippo是它首元素的地址,该元素是一个内含两个int类型值的一维数组。因此,pz必须指向一个内含两个int类型值的数组,而不是指向一个int类型值,其声明如下:
// pz指向一个内含两个int类型值的数组
int (*pz)[2];
以上代码把pz声明为指向一个数组的指针,该数组内含两个int类型值。为什么要在声明中使用圆括号?因为[]的优先级高于*。考虑下面的声明:
// pax是一个内含两个指针元素的数组,每个元素都指向int的指针
int *pax[2];
由于[]优先级高,先与pax结合,所以pax成为一个内含两个元素的数组。然后*
表示pax数组内含两个指针。最后,int表示pax数组中的指针都指向int类型的值。因此,这行代码声明了两个指向int的指针。而前面有圆括号的版本,*
先与pz
结合,因此声明的是一个指向数组(内含两个int类型的值)的指针。
指针之间的赋值比数值类型之间的赋值要严格。例如,不用类型转换就可以把int类型的值赋给double类型的变量,但是两个类型的指针不能这样做。
下面的声明不正确:
int sum(int [][]);
前面介绍过,编译器会把数组表示法转换成指针表示法。例如,编译器会把ar[1]转换成ar+1。编译器对ar+1求值,要知道ar所指向的对象大小。下面的声明:
int sum(int [][4]);
表示ar指向一个内含4个int类型值的数组(在我们的系统中,ar指向的对象占16字节),所以ar+1的意思是“该地址加上16字节”。如果第2对方括号是空的,编译器就不知道该怎样处理。
也可以在第1对方括号中写上大小,如下所示,但是编译器会忽略该值:
int sum(int [3][4]);
有效声明,但是3会被编译器忽略。
因为C规定,数组的维数必须是常量,不能用变量来代替COLS。
变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度。
复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。使用指针记录地址就是一种用法。
把信息传入函数前不必先创建数组,这是复合字面量的典型用法。