第二章 数组名是一个指针常量吗?
数组名是一个指针常量这种观点来源于数组名在表达式计算中与指针的结果等效性。例 如下面的代码: int a[10], *p = a, *q; q = a + 1; q = p + 1;
在效果上看,a + 1 与 p + 1 是相同的,这很容易给人一种 a 就是 p 的假象,但,这仅仅 是假象。鉴于指针常量包含了指针和常量两类概念,我们可以把这个问题分开两部分进行讨 论。
一、数组名是指针吗? 在《C与指针》一书中,作者用一个著名的例子阐述了数组名与指针的不同。在一个文件中
定义:int a[10];然后在另一个文件中声明:extern int *a; 笔者不在这里重复其中的原理,书 中的作者试图从底层操作上阐述数组名与指针的不同点,但笔者认为这个例子存在一些不 足,a 在表达式中会转换为一个非对象的符号地址,而指针 a 却是一个对象,用一个非对象 去跟一个对象比较,有“偷跑”的嫌疑,这个例子只是说明了数组名的非对象性质,只能证 明对象与非对象实体在底层操作上的不同,事实上,如上一章所述,指针也有非对象形态。 笔者认为,无须从底层的角度上花费那么多唇舌,仅仅从字面上的语义就可以推翻数组名是 一个指针的观点。
首先,在C/C++中,数组类型跟指针类型是两种不同的派生类型,数组名跟指针是 两种不同类型的实体,把数组类型的实体说成“是”另一个类型的实体,本身就是荒谬的;
其次,a + 1 在效果上之所以等同于 p + 1,是因为 a 进行了数组到指针的隐式转换,这 是一个转换的过程,是 converted to 而不是 is a 的过程。如果是两个相同的事物,又怎会有 转换的过程呢?当把 a 放在 a + 1 表达式中时,a 已经从一个数组名转换为一个指针,a 是作 为指针而不是数组名参与运算的;
第三,a + 1 与 p + 1 是等效关系,不是等价关系。等价是相同事物的不同表现形式,而 等效是不同事物的相同效果。把数组名说成是指针实际上把等效关系误解为等价关系。
因此,数组名不是指针,永远也不是,但在一定条件下,数组名可以转换为指针。 二、数组名是一个常量吗? 看见这句话有人会觉得奇怪,数组定义之后就不能改变了,数组名不就是个常量吗?在
表达式中,数组名的确可以转换为一个不变的符号地址,但在C中,不变的实体不一定是常 量!而且,C/C++有常量与常量表达式之分,常量与常量表达式是两种不同的实体,但常量 表达式可以作为常量使用。C/C++中的常量虽然有所不同,但都不包括数组或数组名,而且 数组名也不一定是常量表达式。
请在 C90 的编译器中编译如下代码,注意不能是 C99 和 C++的,因为 C99 和 C++不再规 定数组的初始化器必须是常量表达式,会看不到效果: int main( void ) {
static int a[10], b[10]; int c[10], d[10]; int* e[] = { a, b }; /* A */ int* f[] = { c, d }; /* B */ return 0; }
B 为什么不能通过编译?是由于自动数组名并不是常量表达式。在 C 中,常量表达式必 须是编译期的,只在运行期不变的实体不是常量表达式,请看标准的摘录: 6.6 Constant expressions
A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.
c 和 d 是自动数组,首地址在编译期是不可知的,因为这样的对象在编译期还不存在; a 和 b 是静态数组,静态对象从程序开始时就已存在,因此 a 和 b 的首地址在编译期是已知 的,它们都属于常量表达式中的地址常量表达式。
所以,C/C++中的数组名,都不是常量。C 中的数组名,是否常量表达式要视其存储连 续性而定,全局数组、静态数组名都是常量表达式,而自动数组名不是。在 C++中,由于 不再规定常量表达式必须是编译期的,因此 C++的数组名都是常量表达式。
第三章 数组的解剖学
C/C++的数组不同于 VB 等语言的数组,是有层次的,这个层次指的不是维度,而是象 俄罗斯有名的套娃一样,一维套一维,亦即数组的嵌套,数组的元素也是数组,VB 等语言 的数组与之相比更像一个平面。
数组嵌套这个现象从其它语言的角度来看有点奇特,但其实原因也很简单。C/C++的对 象模型并不视数组为某种数值的简单集合,而是对象的聚集,每个元素都是一个对象。元素 为整数对象,就是整数数组,为浮点数对象,就是浮点数数组。然而,数组本身也是一种对 象,因此一个数组也能作为另一个数组的元素。当某个一维数组以一维数组作为元素时,这 个一维数组每个元素都具有数组类型,这个一维数组其实是二维数组,同理,一个以二维数 组作为元素的一维数组其实是三维数组。因此,使用 C/C++数组的时候应该用数组嵌套的观 点去看待。有人据此认为,C/C++的数组不是真正的数组,还有的认为 C/C++没有多维数组, 这些观点都有失偏颇,与其它语言的数组相比,两者只是同一事物的不同实例,是实现方法 的不同,而本质是一样的,C/C++的数组嵌套可视为对数组概念的发展。
现在来看看数组的定义: 6.5.4.2 Array declarators Semantics
If, in the declaration “T Dl.” Dl has the form D [ constant expressionopt ]
这个定义非常简单,其中 T 代表元素类型,D 代表标识符,constant expression 必须为 大于 0 的常量表达式,opt 表示可选,即[]中的内容可以为空,当[]为空时叫不完整类型,表 示这个数组对象的长度未知,不完整数组类型可以在程序的某个地方补充完整。细心的人马 上就会发现,从形式上看,怎么只有一维数组的定义?这个形式如何定义多维数组?刚才说 过,C/C++的数组是数组的嵌套,因此多维数组的定义也反映了这个本质。多维数组的定义 是通过嵌套的一维数组定义构造的。对于一维数组: T D[M]
当元素为一维数组 T[N]时,元素的类型也为数组类型,用 T[N]代替 T,则为: T[N] D[M]
这个语法结构不符合 C/C++数组定义的语法形式,将[N]移动到[M]后,就是正式的二维 数组的定义了: T D[M][N]
其中 D[0]---D[M-1]都是一维数组,具有数组类型 T[N]。各种维度的多维数组可以用同样 的嵌套方法构造出来。
一个一维数组 T[M],经过数组到指针的转换后,类型转换为 T*,二维数组 T[M][N]转换 为指针后,类型转换为 T ( * )[N],有些初学者对 T( * )[N]这种形式较难理解,怎么多了一维, 形式就有这么大的差别呢,其实原理还是跟嵌套有关,二维数组为一维数组的嵌套,元素为 数组类型,因此用 T[N]代替 T,则二维数组转换之后的指针类型为 T[N] *,将[N]移动到*的 右边,就是 T*[N],由于[]的优先级比*高,因此需要加括号,就成为 T( * )[N]了,否则就不 是指针类型,而成了指针数组类型了。
围绕数组名,存在一些有趣的表达式,下面的内容通过讨论这些表达式中较为重要的几 个,来加深对数组的理解。对于二维数组: T a[M][N]
a: 表达式中的 a 的类型转换为 T ( * )[N],代表数组的首地址;
&a:是一个指向二维数组对象的指针,类型为 T ( * )[M][N]。在 C 标准出现之前,一些
早期的实现并不允许&a,因为这些编译器认为此时的 a 转换为一个右值,而&运算符要求一 个左值,因此非法。C 标准委员会鉴于对象的概念已经得到了扩展,而且允许&a 并没有害 处,因此把&运算符作为一个例外写进了数组到指针的转换条款中。这种情况下的 a 代表数 组对象,&a 表示对数组对象取地址,因此&a 的结果跟 a 是相同的,但类型不同。
笔者曾经见过某些观点认为,&a 才是数组的首地址,不是 a。这个观点初看起来似乎 很有道理,一个数组对象的引用,不正是首地址吗?但实际上这种论述是不符合标准的,数 组到指针的转换条款规定,当产生一个 points to the initial element of the array object 的时候, 其前提是由 array of type 到 pointer to type 的转换,但&a 的类型属于 pointer to array of type, 不是 pointer to type,因此真正代表数组首地址的是 a 本身,不是&a。
&a[0][0]:这是数组首元素的地址。&a[0][0]常常被误解为数组 a 的首地址,其实 a[0][0] 只不过由于位置特殊,其地址值才与 a 相同,&a[0][0]是一个 T 类型对象的引用,不是一个 数组对象的引用,而且其类型不是由 array of type 转换得来的,因此其意义不是数组首地址。
a[i](其中 i >= 0 && i < M):从数组嵌套的观点来看,a 是一个一维数组,元素的类型 为数组类型,因此 a[i]的类型为 T[N],在表达式中转换为 T*,是第 i 个一维数组的首地址。
a + 1:a 隐式转换为指针类型 T( * )[N]然后加 1,请记住指针加法是以指针指向对象的大 小为步长的,因此 a + 1 将跨过 N * sizeof( T )个字节。
&a + 1:与 a + 1 同理,&a 类型为 T( * )[M][N],因此&a + 1 的步长为 M * N * sizeof( T )。
第四章 []运算符的本质
下标运算符[]一直被作为数组的专有运算符来介绍,经过长年的应用,人们也早已对这 个用法习以为常,视为跟每天的午餐一样稀松平常的事情。当你很遐意地写下 a[0]表达式的 时候,如果抽空回过头来看看标准中关于下标运算符的条款,你很可能会大吃一惊: 6.5.2.1 Array subscripting Constraints
One of the expressions shall have type ‘‘pointer to object type’’, the other expression shall have integer type, and the result has type ‘‘type’’.
其中一个表达式具有指针类型,而不是数组类型!为什么会这样呢?如果规定为数组类 型,由于表达式中的数组类型会隐式转换为指针类型,两个条款就会产生矛盾,当然,可以 将下标运算符也作为转换规则的例外,但直接规定为指针类型显然能带来更多的好处,而且, 既然数组类型能够转换为指针类型,却不让指针使用下标运算符,会显得无可理喻。从条款 的角度来讲,下标运算符其实是指针运算符。
另一个表达式的类型是 integer,这意味着表达式的值可以是负数,这是由于指针运算 里包含了减法的缘故,但是要注意不应该发生越界的行为。
在条款的上下文中,并没有规定[]运算符两个操作数的顺序,这意味着即使调换两个操 作数的位置,也没有违反标准。这现象还可以从另一个角度进行分析,在表达式中, D[N] 会转换为等价表达式*( D + N ),把 D 和 N 的位置调换,就成了*( N + D ),就是 N[D]了。
考虑如下代码: int a[10],*p = a; p[0] = 10;
( p + 1 )[0] = 20; 0[p + 1] = 10; ( &a )[0][0] = 20; 0[&a][0] = 30; 0[0[&a]] = 40;
a*0+ = “0123456789ABCDEF”*0+; 下面
对各个表达式进行解释: p[0]:就是 a[0];
( p + 1 )[0]:p 移动一个 int 的距离,就是 a[1]; 0[p + 1]:就是( p + 1 )[0];
( &a )[0][0]:这个表达式有点古怪,a 的类型是 int[10],&a 就是 int( * )[10],是一个指 向具有 10 个 int 元素的一维数组的指针,( &a )[0]就是&a 指向的第 0 个元素,类型为 int[10], 因此( &a )[0][0]就是( &a )[0]的第 0 个元素。
0[&a][0]:把第一维的 0 与&a 调换一下,就是 0[&a][0];
0[0[&a]]:再调换 0[&a]与第二维[0]中的 0,就成了 0[0[&a]],跟( &a )[0][0]等价。 最后一个表达式”0123456789ABCDEF”[0]是一个常用的技巧,它可以快速将一个数字 转换为 16 进制字符。”0123456789ABCDEF”是一个字符串字面量,类型是 char[17](在 C 中)或者 const char[17](在 C++中),转换后的指针类型分别为 char*和 const char*,因此” 0123456789ABCDEF”[0]就是第 0 个元素’0’。这个技巧常常用在进制转换中,以下代码将 一个长整数的内存映像转换为 16 进制表示: char* convert( unsigned long Value ) {