C陷阱与缺陷之指针与数组

[player autoplay=”0″]

一个句子哪怕其中的每个单词都拼写正确,而已语法也无懈可击,仍然可能有歧义或者并非书写都希望表达的意思。程序也有可能表面上看上去是一个意思,而实际上的意思却相去甚远。

C语言中指针与数组这两个概念之间的联系是如此密不可分,以至于如果不能理解一个概念,就无法彻底理解另一个概念。而且,C语言对这些概念的处理,在某些方面与其他任何人为人熟知的程序语言都有所不同。

C语言中的数组值得注意的地方有以下两点:

  1. C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然而,C语言中数组的元素可以是任何类型的对象,当然也可以是另外一个数组。这样,要“仿真”出一个多维数组就不是一件难事。

译注:C99标准允许变长数组(VLA)。GCC编译器中实现了变长数组,但细节与C99标准不完全一致。感兴趣的读者可以参看ISO/IEC 9899:1999标准6.7.5.2节,以及Dennis M. Ritchie的Variable-Size Arrays in C。

  1. 对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针的行为定义数组下标的行为。

一但我们彻底弄懂了这两点以及它们所隐含的意思,那么理解C语言的数组运算不过是“小菜一碟”。如果不清楚上述两点内容,那么C语言中的数组运算就可能会给编程者带来许多的困惑。需要特别指出的是,编程者应该具有。需要特别指出的是,编程者应该具备将数组运算与它们对应的指针融汇贯通的能力,在思思有关问题时大脑中对这两种运算与它们对应的指针运算能够自如切换,毫无滞碍。许多程序设计语言中都内建有索引运算,在C语言中索引运算是以指针的形式来定义的。

要理解语言中数组的动作机制,我们首先必须理解如何声明一个数组。例如

这个语句声明了a是一个拥有3个整型元素的数组。类似地,

声明了b是一个拥有17元素的数组,其中每个元素都是一个结构,该结构中包括一个拥有4个整型元素的数组(命名为p)和一个双精度的变量(命名为x)。

现在考虑下面的例子:

这个语句声明了calendar是一个数组,该数组拥有12个数组类型的元素,其中每个元素都是一个拥有31个整型元素的数组。(而不是一个拥有31个数组类型的元素的数组,其中每个元素又是一个拥有12个整型元素的数组。)因此,sizeof(calendar)的值是372(31×12)与sizeof(int)的乘积。

如果calendar不是用于sizeof的操作数,而是用于其他的场合,那么calendar总是转换成一个指向calendar数组的起始元素的指针。要理解上面这句话的含义,我们首先必须理解有关指针的一些细节。

任何指针都是指向某种类型的变量。例如,如果有这样的语句:

就表明ip是一个指向整型变量的指针。又如果声明,

那么我们可以将整型变量i的地址赋给指针ip,就像下面这样:

而且,如果我们给*ip赋值,就能够改变i的取值:

如果一个指针指向的是数组中的一个元素,那么我们只要给这个指针加1,就能够得到指向该数组中下一个元素的指针。同样地,如果我们给这个指针减1,得到就是指向该数组中前一个元素的指针。对于除1之外其他整数的情形,依此类推。

上面这段讨论暗示了这样一个事实:给一个指针加上一个整数,与给该指针的二进制表示加上同样的整数,两者的含义截然不同。如果ip指向一个整数,那么ip+1指向的是计算机内存中的下一个整数,在大多数现代计算机中,它都不同于ip所指向地址的下一个内存位置。

如果两个指针指向的是同一个数组中的元素,我们可以把这两个指针相减。这样做是有意义的,例如:

那么我们可以通过q-p而得到i的值。值得注意的是,如果p与q指向的不是同一个数组的元素,即使它们所指向的地址在内存中的位置正好间隔一个数组元素的整数倍,所得到的结果仍然是无法保证其正确性的。

本节前面已经声明了a是一个拥有3个整型元素的数组。如果我们在应该出现指针的地方,却采用了数组名来替换,那么数组名就被当作指向该数组下标为0的元素的指针。因此如果我们这样写,

就会把数组a中下标为0的元素的地址赋值给p。注意,这里我们并没有写成:

这种写法在ANSI C中是非法的,因为&a是一人指向数组的指针,而p是一个指向整型变量的指针,它们的类型不匹配。大多数早期版本的C语言实现中,并没有所谓的“数组的地址”这一概念,因此&a或者被视为非法,或者就等于a。

继续我们的讨论,现在p指向数组a中下标为0的元素,p+1指向数组a中下标为1的元素,p+2指向数组a中下标为2的元素,依次类推。如果希望p指向数组a中下标为1的元素,可以这样写:

当然,该语句完全等同于下面的写法:

除了a被用作运算符sizeof的参数这一情形,在其他所有的情形中数组名都代表指向数组a中下标为0的元素的指针。正如我们合乎情理的期待sizeof(a)的结果是整个数组a的大小,而不是指向数组a的元素的指针的大小。

从上面的讨论中,我们不难得出一个推论,*a即数组a中下标为0的元素的引用。例如,我们可以这样写:

这个语句指数组a中下标为0的元素的值设置为84。同样道理,(a+1)是数组中下标为1的元素的引用,依次类推。概而言之,(a+1)即数组中下标为i的元素的引用;这种写法是如此常用,因此它被简记为a[i]。

正是这一概念让许多C语言新手难于理解。实际上,由于a+i与i+a的含义一样,因此a[i]与i[a]也具有相同的含义。也许某些汇编语言程序员会发现后一写法很熟悉,但我们绝对不推荐这种写法。

现在我们可以考虑“二维数组”了,正如前面所讨论的,它实际上是以数组为元素的数组。尽管我们也可以完全依据指针编写操纵一维数组的程序,这样做在一维情形下并不困难,但是对于二维数组从记法上的便利性来说,采用下标形式就几乎是不可替代的。还有,如果我们仅仅使用指针来操纵二维数组,我们将不得不与C语言中最为“晦暗不明”的部分打交道,并常常遭遇潜伏着的编译器bug。

让我们回过头来再看前面的几个声明:

然后,考一考自己,calendar[4]的含义是什么?

因为calendar是一个有着12个数组类型元素的数组,它的每个数组类型元素又是一个有着31个整型元素的数组,所以calendar[4]是calendar数组的第5个元素,是calendar数组中12个有着31个整型元素的数组之一。因此,calendar[4]的行为也就表现为一个有着31个整型元素的数组的行为。例如,sizeof(calendar[4])的结果是31与sizeof(int)的乘积。又如:

这个语句使指针p指向了数组calendar[4]中下标为0的元素。

如果caldendar[4]是一个数组,我们当然可以通过下标的形式来指定这个数组中的元素,就像下面这样,

我们也确实可以这样做。还是与前面类似的道理,这个语句可以写成下在这们而表达的意思保持不变:

这个语句还可以进一步写成,

从这里我们不难发现,用带方括号的下标形式很明显地要比完全用指针来表达简便得多。

下面我们再看:

这个语句是非法的。因为calendar是一个二维数组,即“数组的数组”,此外的上下文中使用calendar名称会将其转换为一个指向数组的指针;而p是一个指向整型变量的指针,这个语句试图将一种类型的指针赋值给另一种类型的指针,所以是非法的。

很显然,我们需要一种声明指向数组的指针的方法。经过第2章中对类似问题不厌其烦的讨论,构造出下面的语句应该不需要费多大力气:

这个语句的实际的效果是,声明了*ap是一个拥有31个整型元素的数组,因此ap就是一个指向这样的数组指针。因而,我们可以这样写:

这样monthp将指向数组calendar的第1个元素,也就是数组calendar的12个有着31个元素的数组类型无素之一。

假定在新的一年开始时,我们需要清空calendar数组,用下标的形式可以很容易做到:

上面的代码如果采用指针应该如何表示呢?我们可以很容易地把

表示为

但是真正有关的部分是哪些呢?

如果指针monthp指向一个拥有31个整型元素的数组,而calendar的元素也是一个拥有31个整型元素的数组,因此就像在其他情况中我们可以使用一个指针遍历一个数组一样,这里我们可以同样使用指针monthp以步进的方式遍历数组calendra:

同样地,我们可像处理其他数组一样,处理指针monthp所指向的数组的元素:

到目前为止,我们一路行来几乎是“如履薄冰”,而且已经走得太远,在我们跌跤之前,最好趁早悬崖勒马。尽管本节中最后一个例子是ANSI C程序,但是作者还没有找到一个能够让程序顺利通过编译的编译器(译注:现在大多数的C编译器能够接受上面例子的代码,在CodeWarrior 10.6下编译通过)。上面例子的讨论虽然有些偏离本书的主题,但是这个例子能够很多地揭示出C语言中数组与指针之间的独特的关系,从而更清楚明白地阐述这两个概念。

未经允许不得转载:TacuLee » C陷阱与缺陷之指针与数组

赞 (0)

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址