3.6 应该记住:数组和指针是不同的事物
3.6.1 为什么会引起混乱
首先,请允许我强调一下本章的重要观点。
C 语言的数组和指针是完全不同的。
大家都说 C 语言的指针比较难,可是真正地让初学者“挠墙”的,并不是指针自身的使用,而是“混淆了数组和指针”。此外,很多“坑爹”的入门书对指针和数组的讲解也是极其混乱。
比如,K&R 中就有下面一段文字(p.119),
C 语言的指针和数组之间有很强的关联关系,因此必须将指针和数组放在一起讨论。
很多 C 程序员认为“数组和指针是几乎相同的事物”,这种认识是引起 C 的混乱的主要原因。
从图 3-17 中可以一目了然地看出,数组是一些对象排列后形成的,指针则表示指向某处。它们是完全不同的。
图 3-17 数组和指针
带着“数组和指针是几乎相同的事物”这样的误解,初学者经常写出下面这样的代码:
int *p;
p[3] = …… ←突然使用没有指向内存区域的指针
——自动变量的指针在初期状态,值是不定的。
char str[10];
┊
str = "abc"; ←突然向数组赋值
——数组既不是标量,也不是结构体,不能临时使用。
int p[]; ←使用空的[]声明局部变量
——只有在“函数的形参的声明”中,数组的声明才可以被解读成指针。
对于数组和指针,它们在哪些地方是相似的,又在哪些地方是不同的——不好意思,可能在下面会出现和前面重复的内容。
3.6.2 表达式之中
在表达式中,数组可以被解读成指向其初始元素的指针。所以,可以写成下面这样:
int *p;
int array[10];
p = array; ←将指向array[0]的指针赋予p
可是,反过来写成下面这样:
array = p;
就是不可以的。确实,在表达式中 array
可以被解读成指针,可是,本质上它其实是被解释成了&array[0]
,此时的指针是一个右值*。
* 此时的指针是右值这个理由之外,在标准中,数组也不是“可变更的左值”。
比如,对于 int
类型的变量 a
,a = 10;
这样的赋值是可以的,但肯定没有人想做 a + 1 = 10;
这样的赋值吧。尽管 a
和 a + 1
都是 int
,但是 a + 1
没有对应的内存区域,只是一个右值,所以不能被赋值。同样的道理,array
也不能被赋值。
此外,对于下面这个指针,
int *p;
如果 p 指向了某个数组,自然可以通过 p[i]
的方式进行访问,但这并不代表 p
就是数组。
p[i]
只不过是*(p + i)
的语法糖,只要 p
正确地指向一个数组,就可以通过 p[i]
对数组的内容进行访问,就像图 3-18 表现的这样。
图 3-18 使用指针访问数组
如果是“指针的数组”和“数组的数组”,就会有很大的不同。
char *color_name[] = { ←指针的数组
"red",
"green",
"blue",
};
对以上的代码进行图解(参照图 3-19),
图 3-19 指针的数组
char color_name[][6] = { ←数组的数组
"red",
"green",
"blue",
};
对以上的代码进行图解(参照图 3-20),
图 3-20 数组的数组
以上两种情况都可以用 color_name[i][j]
的方式对数组进行访问,但是内存中数据的布局是完全不同的。
3.6.3 声明
只有在声明函数的形参的时候,数组的声明才能解读成指针的声明(参照 3.5.1 节)。
以上的语法糖,与其说使 C 变得更加容易理解,倒不如说它使 C 语言的语法变得更加混乱。是不是有很多人这么想?我就是其中的一个*。而且 K&R 的说明更是使这种混乱局面雪上加霜。
* 虽然使用这个语法糖可以让多维数组作为参数被传递时更容易理解……
在不是声明函数的形参的时候,数组声明和指针的声明是不可能相等的。
使用 extern
的时候是最容易出现问题的(参照 3.5.2 节)。另外,声明局部变量或者结构体的成员时,写成
int hoge[];
会引起语法错误*。
* 对于结构体的成员,在 ISO C99 中是允许这种写法的。
存在数组初始化表达式的情况下,可以使用空的[]
,但这是因为编译器能够计算出数组元素的个数,所以可以省略书写元素个数。仅此而已,这种特征和数组扯不上任何关系。
要 点
【非常重要!!】
数组和指针是不同的事物。