作为表达式结果的数组
在讨论串字面量本质的时候,我们发现,在转换概念的范围内,所谓数组不光是指在程序中被我们明确定义的数组,也可以是表达式计算的结果。如原始表达式串字面量的结果就是字符数组。果真如此吗?我们来看看下面的情况。
试想对于一个数组a,表达式 (&a)[0] 和表达式 &a[0] 有什么不同?
&a[0] 这个表达式我们在前面已经分析过了,它的结果是一个指向a的首元素的指针。
而表达式 (&a)[0] 的不同之处在于提高了取址操作符&的优先级。于是,在 (&a)[0] 中,数组 a 作为操作符&的操作数,不会发生转换。子表达式 &a 是取数组a的地址得到指向该数组的指针,而接下来的运算就是指针运算了,结果便是数组a本身。
那么作为表达式 (&a)[0] 的结果的数组会不会有转换行为呢?答案是肯定的。
空口无凭,再看代码:
char a[4] = "abc"; char * p; p = &a[0]; p = (&a)[0]; printf("%d\n", (&a)[0] == &a[0]); /* 输出1*/ printf("%d\n", sizeof((&a)[0])); /* 输出 4,数组a的大小 */ printf("%d\n", sizeof(&a[0])); /* 输出 8,x64系统中指针的大小 */ char (*pa)[4]; pa = &((&a)[0]);
表达式 &a[0] 的结果是char * 型的指针,它可以赋值给同类型的指针p是理所当然的。但奇怪的是,表达式 (&a)[0] 结果是一个数组,竟然也可以赋值给指针p。考虑下转换规则,这个情况就完全合理了,这说明作为计算结果的数组,也会在符合转换条件的情况下发生转换。
将这两个表达式进行逻辑比较,得到的结论是它们是相同的,这也说明表达式 (&a)[0] 发生了转换。我们再尝试将这两个表达式分别作用于操作符 sizeof ,根据规则,作为sizeof的操作数,它们不会发生转换,事实确是如此。
对一个数组取地址会得到指向该数组的指针,我们将表达式 (&a)[0] 作为操作符&的操作数(此时也没有转换)得到的确实是指向数组类型的指针。如果,我们脑洞大一些,将 &a[0] 作为&的操作数会怎么样? &a[0] 是一个指针,可能我们会觉得是不是会得到一个指向指针的指针?答案是不会! &a[0] 是一个指针没错,但没有确切地址,不是lvalue,无法取址, &(&a[0]) 会编译错误!
转换的基础数组和指针之所以有这么微妙的关系,内存分配才是关键因素。上面讨论的表达式 (&a)[0] 的结果就是数组a本身,如果我们把方括号中的整数0改为1或者2这样的值,那又意味着什么?
char a[4] = "abc"; char * p = &a[1]; /* p指向数组a的第二个元素 */ p = (&a)[1];/* p不指向数组第二个元素 */ printf("%d\n", sizeof((&a)[1])); /* 输出数组大小 4 */ printf("%d\n", (&a)[1] == &a[0]+4 ); /* 输出 1 */ char (*pa)[4] = &a; printf("%c\n", *(pa+1)[0]); /* 即((&a)[1]))[0], 输出值不确定 */
我们先看看表达式 (&a)[1] 的结果是什么,首先, &a 将得到指向数组a的一个指针,而指针的下标运算 [1] 表示指针所指空间相邻的同大小空间内的对象,虽然我们知道,数组a的旁边有什么并不确定,但至少不应该是个和a一样的数组,因为我们此前并没有定义一个数组的数组。尽管如此,我们发现系统依然照着a的样子,在内存中为我们找到了它旁边一块同样大的空间,并“假装”它保存的也是一个同类型的数组,我们用sizeof测试时会得到数组大小的值。而且 (&a)[1] 这个“数组”还可以转换为一个指向它首元素的指针,我们将它和 &a[0]+4 (可以看作指向从a的首元素起后面的第5个元素的指针,即&a[4])比较发现,它们是相等的。
我们尝试按照取数组a中元素那样,从a旁边这个并不存在的数组中读取数据,虽然输出值没有意义,但这却并非是一种非法操作,C语言允许我们这么做。
顺便说下,数组下标在C语言中是被定义为指针运算的,ISO/IEC 9899:201 §6.5.2.1 [Array subscripting] para 2 中的的原文如下:
A postfix expression followed by an expression in square brackets [] is a subscripted designation of an element of an array object. The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that apply to the binary + operator, if E1 is an array object (equivalently, a pointer to the initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th element of E1 (counting from zero).
因此像 a[-1] 这样的负数下标虽然没有意义,但也是合法的。小于0 或大于等于数组元素数的下标值是无意义的,那我们如何看待这些越界的下标呢?实际上,下标是定义给指针的。 E1[E2] 中的两个子表达式E1和E2一个是指针类型一个是整数类型[^ISO/IEC 9899:201x §6.5.2.1 para1],因为转换规则的存在,数组才可以成为E1和E2中的一个。
为什么说是E1和E2中的一个?难道数组类型的不应该是E1吗?看下面的代码示例:
int a[5] = { 0, 1, 2, 3, 4 }; cout << a[2] << endl; // 输出 2 cout << 2[a] << endl; // 输出 2 cout << 5["abcdef"] << endl; // 输出 f
对于a[2]的输出我们没有异议,但后面两个表达式有点"诡异"。前面在引用C11中下标规范时提到, E1[E2] 与 (*((E1)+(E2))) 是等同的(identical )。那么就会有:
E1[E2] (*((E1)+(E2))) 下标定义 (*((E2)+(E1))) 加法交换律 E2[E1] 下标定义所以, E1[E2] 与 E2[E1] 是等同的。因此,a[2]和2[a]都输出2,而 5["abcdef"] 其实是 "abcdef"[5] ,结果是串字面量(字符数组)中的第5个(从0计)字符f。
我们发现,数组和指针在从内存访问数据上并没有本质区别,数组貌似仅意味着数据连续不间断的存放而已,而数组类型都有相应的指针类型对应,如int型数组对应有指向int型的指针,这种类型信息提供了指针访问内存时每个步长的移动跨度。除此之外,数组好像也不能再给我们太多信息了,它甚至不能做出越界检查。
从转换来看,数组可谓是C语言世界的二等公民,在一切可以的情况下,它都转换为一个指向自身首元素指针。
作为函数参数的数组函数调用中,作为实参的数组会在函数调用前被转换为一个指向其首元素的指针。因此,数组永远都不会真正地被传递给一个函数。这样的话,看上去将函数的参数声明未数组类型就会变得没有意义。事实上,函数的参数类型列表中但凡被声明为数组类型的,在编译期间会被调整为对应的指针类型,这意味着,在函数体内操作一个被声明为数组的形参,其实是在操作一个指针。
有关函数参数中的数组的调整和转换在C11规范中有多处说明[^函数参数数组参考],感兴趣可自行查看。
需要注意的是,虽然编译器会做调整,但我们不应将数组类型的形参声明都改为指针类型。因为数组和指针意义不同,在定义函数时不要理会编译器的调整有助于我们写出意义更加明确可读性更高的代码。
void f(char str[]) /* 会被调整为: void f(char *str) */{ cout << sizeof(str) << endl; /* str是指针,而不是数组 */ if(str[0] == '\0') str = "none"; /* 这里并不是数组被赋值,而是指针str被赋值 */ cout << str << endl; } int main(){ char a[3]; f(a); a[0] = 'x'; f(a); return 0; }
在上面例子中,函数f声明了一个字符数组型的参数str,实际上在编译时str会被调整为指向字符的指针类型。在f内,尽管str被我们显式地声明为数组类型,但函数内部它就是作为一个指针来用的,sizeof操作符给出的结果是指针大小,而且我们看到str可以被直接赋值,要知道数组是没有这个特性的。
字符数组a出现在函数调用时的实参列表中时,并不代表a本身,而是被转换为指向a首元素的指针,因此真正的实参是一个指针而非数组a。如果忽略f中sizeof那一行,那么f的功能就可被描述为:输出一个字符数组,如果字符数组为空则输出“none”。