第九章 再论数组
9.1 什么时候数组与指针相同
声明本身还可以进一步分成3种情况:
外部声明(external array)的声明。
数组的定义(记住,定义是声明的一种特殊情况,它分配内存空间,并可能提供一个初始值)。
函数参数的声明。
所有作为函数参数的数组名总是可以通过编译器转换为指针。
9.2 为什么会发生混淆
作为函数定义的形式参数,char s[]和 char* s是一样的。
数组下标表达式总是可以改写为带有偏移量的指针表达式。
char my_array[10];
char *my_ptr;
……
i = strlen( my_array);
j = strlen( my_ptr);
printf ( “%s %s”, my_ptr, my_array);
它清楚地展示了数组和指针的可互换性。
printf(“ array at location %x holds string %s”, a, a);
同一条语句中,既把数组名作为一个地址(指针),又把它作为一个字符数组。
什么时候数组和指针相同:
规则1. 表达式中的数组名(与声明不同)被编译器当做一个指向该数组第一个元素的指针。
规则2. 下标总是与指针的偏移量相同。
规则3. 在函数参数的声明中,数组名被编译器当做指向该数组第一个元素的指针。
int a[10], *p, i=2;
可以通过以下任何一种方法来访问a[i];
p = a; p[i];
p = a; *(p + i);
p = a + i; *p;
对数组的引用如a[i]在编译时总是被编译器改写成*(a + i)的形式。
在表达式中,指针和数组是可以互换的,因为它们在编译器里的最终形式都是指针,并且都可以进行取下标操作。
在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向该数组第一个元素的指针形式。编译器向函数传递数组的地址,而不是整个数组的拷贝。这种隐性转换意味着三种形式是完全等同的。因此,在函数my_function()的调用上,物理实参是数组还是真的指针都是合法的。
my_function(int *turnip) {……}
my_function(int turnip[]) {……}
my_function(int turnip[200]) {……}
9.3 为什么C语言把数组形参当作指针
之所以要把传递给函数的数组参数转换为指针是出于效率的考虑。
在C语言中,所有非数组形式的数据实参均以传值形式调用。
在下列的定义中:
my_function(int *turnip) {……}
my_function(int turnip[]) {……}
my_function(int turnip[200]) {……}
int my_int;
int *my_int_ptr;
int my_int_array[10];
你可以合法地使用下列任何一个实参来调用上面任何一个原型的函数。它们常常用于不同的目的。
相反,如果出于func()函数内部,就没有一种容易的方法分辨这些不同的实参,因此也无法知道调用该函数是出于何种目的。
当然,在函数内部使用指针,所能进行的对数组的操作几乎跟传递原原本本的数组没有差别,只不过,如果想用sizeof来获取数组的长度,所得到的结果不正确而已。
如果想让代码看上去清楚明白,就必须遵循一定的规则!我们倾向于始终把参数定义为指针。因为这是编译器内部所使用的形式。
注意,有一样操作只能在指针里进行而无法在数组中进行,那就是修改它的值。数组名是不可修改的左值,它的值是不能改变的。
语句array = array2;将引起一个编译时错误,错误信息是“无法修改数组名”。但是arr =array2却是合法的,因为arr虽然声明为一个数组但实际上却是一个指针。
9.4 数组片段的下标
可以让指针指向任何一个元素,这样传递给函数的就是从该元素之后的数组片段。
9.5 数组和指针可交换性的总结
1. 用a[i]这样的形式对数组进行访问总是被编译器“改写”或解释为像*(a + i)这样的指针访问。
2. 指针始终就是指针。它绝不可以改写成数组。可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。
3. 在特定的上下文中,也就是它作为函数的参数(也只有这种情况),一个数组的声明可以看做是一个指针。
4. 因此,当把一个数组定义为函数的参数时,可以选择把它定义为数组,也可以定义指针。不管选择哪种方法,在函数内部事实上获得的都是一个指针。
5. 在其他所有情况下,定义和声明必须匹配。如果定义一个数组,在其他文件对它进行声明时,也必须把它声明为数组,指针也是如此。
9.6 C语言的多维数组
C语言的方法多少有点独特:定义和引用多维数组唯一的方法就是使用数组的数组。尽管术语上称作“多维数组”,但C语言实际上只支持“数组的数组”。
int apricot[2][3][5];
可以按图所示的任何一种方法为它在内存中定位。
在C语言的多维数组中,最右边的下标是最先变化的,这个约定称为“行主序”。由于“行/列主序”这个术语只适用于恰好是二维的多维数组,所以更确切的术语是“最右的下标先变化”。
如果在数组的定义里未标明它的长度,C语言规定按照初始化值的个数来确定数组的长度。
short cantaloupe[2][5] = {
{10, 12, 3, 4, -5}
{31, 22, 6, 0, -5}
};
int rhubarb[][3] = { { 0, 0, 0}, {1, 1,1},};
注意,可以在最后一个初始化值的后面加一个逗号,也可以省略它。同时,也可以省略左边下标的长度(也只能是最左边的下标),编译器会根据初始化值的个数推断出它的长度。
如果数组长度比所提供的初始化值的个数要多,剩余的几个元素会自动设置为0,。如果元素的类型是指针,那么他们被初始化为NULL;如果元素的类型是float,那么它们被初始化为0.0。
下面是一种初始化二维字符串数组的方法:
char vegetables[][9] = { “beet”, “barley”, “basil”, “broccoli”, “beans”};
一种有用的方法是建立指针数组。字符串常量可以用作数组初始化值,编译器会正确地把各个字符存储于数组中的地址。因此:
char *vegetables[]={ “beet”, “barley”, “basil”, “broccoli”, “beans”}; /*没问题*/
只有字符串常量才可以初始化指针数组。指针数组不能由非字符串的类型直接初始化:
int *weights[]= { {1, 2, 3, 4, 5}, {6, 7}, {8, 9, 10} }; /*无法编译通过*/
如果想用这种方法来对数组进行初始化,可以创建几个单独的数组,然后用这些数组名来初始化原先的数组。
int row_1[]={1, 2, 3, 4, 5,-1};/*-1是行结束标志*/
int row_2[]={6, 7, -1};
int row_3[]={8, 9, 10, -1};
int *weight[] = { row_1, row_2, row_3};