常用概念
(1) 指针的数组:char *color_name[] = {"red", "green", "blue"};
(2) 数组的数组:char color_name[][6] = {"red", "green", "blue"};
(3) 指向 int 的指针:int *age = malloc(sizeof(int));
(4) 指向 int 的数组(元素个数3)的指针:int (*array_p)[3]
一、数组 和 指针 的微妙关系
数组 是指将固定个数、相同类型的变量排列起来的对象。
正如之前说明的那样,给指针加 N,指针前进“当前指针指向的变量类型的长度 X N”。
因此,给指向数组的某个元素的指针加 N 后,指针会指向 N 个之后的元素。
#include <stdio.h> int main(void) { int array[5]; int *p; int i; /* 给数组 array 的各元素设定值 */ for (i = 0; i < 5; i++) { array[i] = i; } /* 输入数组各元素的值(指针版) */ for (p = &array[0]; p != &array[5]; p++) { printf("%d\n", *p); } return 0; }
运行结果如下:
0 1 2 3 4
从第 16 行开始一个 for 循环,最初指针 p 指向 array[0],通过 p++ 顺序地移动指针,引导指针指向 array[5](尽管它不存在)
使用 ++运算符给指针加 1,指针前进 sizeof(int) 个字节。
此外,第 16~18 行的代码也可以换一种写法(我们可以称之为“改写版”)。
p = &array[0]; for (i = 0, i < 5; i++) { printf("%d\n", *(p + i)); }
这种写法,指针并没有一步步前进,而是固定的,只是在打印的时候加 i。
话说回来,你觉得这种写法容易阅读吗?
至少在我看来,无论写成 p++,还是 *(p + i),都不容易读。还是最初的例子中 a[i] 这样的方式更容易理解。
实际上,本书主张的是“因为利用指针运算的写法不容易阅读,所以让我们 抛弃这种写法吧”。
二、表达式中,下标运算符[] 和 数组 是没有关系的
在前一小节的“改写版”例程中,像下面这样将指针指向数组的初始元素。
p = &array[0];
其实也可以写成下面这样
p = array;
对于这种写法,很多 C 语言的入门书籍是这样说明的:
在 C 中,如果在数组名后不加[],单独地只写数组名,那么此名称就表示“指向数组初始化元素的指针”。
在这里,我可以负责地告诉你,上面的说明是错误的。
在 C 的世界里,事到如今你再去否定“数组名后不加[],就代表指向初始元素的指针”这个“强大的”误解显得有点无奈。对于这种已经深入人心的观点,你突然放言它其实是个误解,可能很多人无法接受。下面让我们依法来证明。
将 &array[0] 改写成 array,“改写版”的程序甚至可以写成下面这样:
p = array; // 只是改写了这里,可以 ... for (i = 0; i < 5; i++) { printf("%d\n", *(p + i)); }
另外,程序中 *(p + i) 也可以写成 p[i]。
p = array; for (i = 0; i < 5; i++) { printf("%d\n", p[i]); }
也就是说,*(p + i)
和 p[i]
是同样的意思。可以认为后面的写法是前面的简便写法。
在这个例子中,最初通过 p = array; 完成了向 p 的赋值,但之后 P 一直没有发生更改。所以,早知道如此,何必当初偏要多声明一个 p,还不如一开始就写成 array 呢。
for (i = 0; i < 5; i++) { printf("%d\n", array[i]); }
呀,好像又回去了呢。
结论就是,
p[i]
这种写法只不过是 *(p + i)
这种写法的简便写法,除此之外,它毫无意义。array[i]
和 p[i]
有什么不一样吗?array[i] 也可以像 p[i] 一样,将 array 解读成“指向数组的初始元素的指针”。
也就是说,存在
int array[5];
这样的声明的时候,“一旦后面不追加[],只写 array”并不代表要使 array 具有指向数组第 1 个元素的指针的含义,无论加不加 [],
在表达式中,数组名都可以被解读成指针。
顺便说一下,对于这个规则来说,有三个小的例外,我们会在第 3 章作详细说明。
你可以认为这是一个哗众取宠的异端邪说,但至少在语法上,数组 下标运算符[] 和 数组无关。
这里也是 C 的数组下标从 0 开始的理由之一。
【要点】
【非常重要!!】
在表达式中,数组可以解读成“指向它的初始元素的指针”。尽管有 3 个小例外,但是这和在后面加不加[]没有关系。
【要点】
p[i] 和 *(p + i) 的简便写法。
下标运算符[]原本只有这种用法,它 和 数组无关。
需要强调的是,认为 [] 和 数组 没有关系,这里的 [] 是指在表达式中出现的下标运算符[]。
声明中的[],还是表达式数组的意思。也就是说,声明中的 [] 和 表达式 中的 [] 意义完全不同。表达式中的 * 和 声明中的 * 意义也是完全不同的。这些现象使得 C 语言的声明在理解上变得更加扑朔迷离...,第 3 章将会进行详细说明。
此外,如果将 a + b 改写成 b + a,表达式的意义没有发生改变,所以你可以将 *(p + i) 写成 *(i + p)。其次,因为 p[i] 是 *(p + i) 的简便写法,实际上它也可以写成 i[p]。
引用数组元素的时候,通常我们使用 array[5] 这样的写法。其实,就算你写成 5[array],还是可以正确地引用到你想要的元素。可是,这种写法实在太另类了,它不能给我们带来任何好处。
【要点】
p[i] 可以写成 i[p],但不推荐这样写。
三、什么是指向数组的指针
“数组”和“指针”都是派生类型。它们都是由基本类型开始重复派生生成的。
也就是说,派生出“数组”之后,再派生出“指针”,就可以生成“指向数组的指针”。
一听到“指向数组的指针”,有人也许要说:
这不是很简单嘛,数组名后不加[],不就是“指向数组的指针”吗?
抱有这个想法的人,请将 1.3 节的内容重新阅读一下!的确,在表达式中,数组可以被解读成指针。但是,这不是“指向数组的指针”,而是“指向数组初始元素的指针”。
int (*array_p)[3]; // array_p 是指向 int 数组(元素个数3)的指针。
根据 ANSI C 的定义,在数组前加上&,可以取得“指向数组的指针” * 。因此,
* 这里是“数组可以解读成指向它初始元素的指针”这个规则的一个例外(参照 3.3.3 节)
int array[3]; int (*array_p)[3]; array_p = &array; // ←数组添加&,取得“指向数组的指针”
这样的赋值是没有问题的,因为类型相同。
可是,如果进行
array_p = array;
这样的赋值,编译器就会报出警告。
“指向 int 的指针”和“指向 int 的数组(元素个数 3)的指针”是完全不同的数据类型。
但是,从地址的角度来看,array 和&array 也许就是指向同一地址。但要说起它们的不同之处,那就是它们在做指针运算时结果不同。
在我的机器上,因为 int 类型的长度是 4 个字节,所以给“指向 int 的指针”加 1,指针前进4 个字节。但对于“指向 int 的数组(元素个数 3)的指针”,这个指针指向的类型为“int 的数组(元素个数 3)”,当前数组的尺寸为 12 个字节(如果 int 的长度为 4 个字节),因此给这个指针加1,指针就前进12个字节(参照图3-6)。
道理我明白了,但是一般没有人这么用吧?
可能有人存在以上的想法。但真的有很多人就是这么用的,只不过是自己没有意识到。为什么这么说呢?在后面的章节中将会说明。
四、C语言中,不存在多维数组
在 C 中,可以通过下面的方式声明一个多维数组:
int hoge[3][2];
我想企图这么干的人应该很多。请大家回忆一下 C 的声明的解读方法,上面的声明应该怎样解读呢?
是“int 类型的多维数组”吗?
这是不对的。应该是“int 的数组(元素个数 2)的数组(元素个数 3)”。也就是说,即使 C 中存在“数组的数组”,也不存在多维数组 * 。
* 在 C 标准中,“多维数组”这个词最初出现在脚注中,之后这个词也会不时地出现在各个角落。尽管“不存在多维数组”这个观点会让人感觉有些极端,但如果你不接受这个观点,对于C 的类型模型的理解,可能就会比较困难。
“数组”就是将一定个数的类型进行排列而得到的类型。“数组的数组”也只不过是派生源的类型恰好为数组。图 3-7 是“int 的数组(元素个数 2)的数组(元素个数 3)”。
要 点
C 语言中不存在多维数组。
看上去像多维数组,其实是“数组的数组”。
对于下面的这个声明:
int hoge[3][2];
可以通过 hoge[i][j]的方式去访问,此时,hoge[i]是指“int 的数组(元素个数 2)的数组(元素个数 3)”中的第 i 个元素,其类型为“int 数组(元素个数 2)”。当然,因为是在表达式中,所以在此时此刻,hoge[i]也可以被解读成“指向 int 的指针”。
关于这一点,3.3.5 节中会有更详细的说明。
那么,如果将这个“伪多维数组”作为函数的参数进行传递,会发生什么呢?
试图将“int 的数组”作为参数传递给函数,其实可以直接传递“指向 int 的指针”。这是因为在表达式中,数组可以解释成指针。
因此,在将“int 的数组”作为参数传递的时候,对应的函数的原型如下:
void func(int *hoge);
在“int 的数组(元素个数 2)的数组(元素个数 3)”的情况下,假设使用同样的方式来考虑,
int 的数组(元素个数 2)的数组(元素个数 3)
其中下划线部分,在表达式中可以解释成指针,所以可以向函数传递
指向 int 的数组(元素个数 2)的指针
这样的参数,说白了它就是“指向数组的指针”。
也就是说,接收这个参数的函数的原型为:
void func(int (*hoge)[2]);
直到现在,有很多人将这个函数原型写成下面这样:
void func(int hoge[3][2]);
或者这样:
void func(int hoge[][2]);
其实,
void func(int (*hoge)[2]);
就是以上两种写法的语法糖,它和上面两种写法完全相同。
关于将数组作为参数进行传递这种的情况下的语法糖,在 3.5.1 节中会再一次进行说明。
五、应该记住:数组和指针是不同的事物 【指针的数组 、数组的数组】
5.1 为什么会引起混乱
首先,请允许我强调一下本章的重要观点。
C 语言的数组和指针是完全不同的。
大家都说 C 语言的指针比较难,可是真正地让初学者“挠墙”的,并不是指针自身的使用,而是“混淆了数组和指针”。此外,很多“坑爹”的入门书对指针和数组的讲解也是极其混乱。
比如,K&R 中就有下面一段文字(p.119),
C 语言的指针和数组之间有很强的关联关系,因此必须将指针和数组放在一起讨论。
很多 C 程序员认为“数组和指针是几乎相同的事物”,这种认识是引起 C 的混乱的主要原因。
从图 3-17 中可以一目了然地看出,数组是一些对象排列后形成的,指针则表示指向某处。它们是完全不同的。
带着“数组和指针是几乎相同的事物”这样的误解,初学者经常写出下面这样的代码:
int *p; p[3] = …… ←突然使用没有指向内存区域的指针
——自动变量的指针在初期状态,值是不定的。
char str[10]; ┊ str = "abc"; ←突然向数组赋值
——数组既不是标量,也不是结构体,不能临时使用。
int p[]; ←使用空的[]声明局部变量
——只有在“函数的形参的声明”中,数组的声明才可以被解读成指针。
对于数组和指针,它们在哪些地方是相似的,又在哪些地方是不同的——不好意思,可能在下面会出现和前面重复的内容。
5.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 表现的这样。
如果是“指针的数组”和“数组的数组”,就会有很大的不同。
char *color_name[] = { ←指针的数组 "red", "green", "blue", };
对以上的代码进行图解(参照图 3-19),
char color_name[][6] = { ←数组的数组 "red", "green", "blue", };
以上两种情况都可以用 color_name[i][j]的方式对数组进行访问,但是内存中数据的布局是完全不同的。
5.3 声明
只有在声明函数的形参的时候,数组的声明才能解读成指针的声明(参照 3.5.1 节)。
以上的语法糖,与其说使 C 变得更加容易理解,倒不如说它使 C 语言的语法变得更加混乱。是不是有很多人这么想?我就是其中的一个 * 。而且 K&R 的说明更是使这种混乱局面雪上加霜。
* 虽然使用这个语法糖可以让多维数组作为参数被传递时更容易理解……
在不是声明函数的形参的时候,数组声明和指针的声明是不可能相等的。
使用 extern 的时候是最容易出现问题的(参照 3.5.2 节)。另外,声明局部变量或者结构体的成员时,写成
int hoge[];
会引起语法错误 * 。
* 对于结构体的成员,在 ISO C99 中是允许这种写法的。
存在数组初始化表达式的情况下,可以使用空的[],但这是因为编译器能够计算出数组元素的个数,所以可以省略书写元素个数。仅此而已,这种特征和数组扯不上任何关系。
要 点
【非常重要!!】
数组和指针是不同的事物。