3.8.5 数组

数组是一种复合类型,因为它们允许在一个单一的标识符下把变量结合在一起,一个接着一个。如果写出

3.8.5 数组 - 图1

就为10个int变量创建了一个接一个的存储空间,但是每一个变量并没有单独的标识符。相反,它们都集结在名字a下。

要访问一个数组元素,可以使用定义数组时所使用的方括号语法:

3.8.5 数组 - 图2

不过,必须记住,尽管a的大小是10,但是要从零开始选择数组元素(有时这被称为零指针),所以只可以选择数组元素0~9,如下所示:

3.8.5 数组 - 图3

访问数组是很快的。但是,如果下标超出数组的界限,这就不安全了,这可能会访问到别的变量。另一个缺陷是必须在编译期定义数组的大小;如果想在运行期改变大小,则不能使用上面的语法(C有一种动态创建数组的方式,但是这会造成严重的混乱)。在前面一章中介绍的C++向量提供了类似数组的对象,它能自动调整自身的大小,所以如果数组的大小在编译期不能确定的话,这是比较好的解决方法。

可以生成任何类型的数组,甚至是struct类型的:

3.8.5 数组 - 图4

注意:struct中的标识符i如何与for循环中的i无关。

为了知道数组中的相邻元素之间的距离,可以打印出地址如下:

3.8.5 数组 - 图5

3.8.5 数组 - 图6

当运行程序时,会看到每一个元素和前一个元素都是相距int大小的距离。也就是说,它们是一个接一个存放的。

3.8.5.1 指针和数组

3.8.5 数组 - 图7

数组的标识符不像一般变量的标识符。一方面,数组标识符不是左值,不能给它赋值。它只是一个进入方括号语法的手段,当给出数组名而没有方括号时,得到的就是数组的起始地址:

运行这个程序时,会看到这两个地址(因为没有转换为long,所以它以十六进制的形式打印出来)是一样的。

因此可以把数组标识符看做是数组起始处的只读指针。尽管不能改变数组标识符指向,但是可以另创建指针,使它在数组中移动。事实上,方括号语法和指针一样工作:

3.8.5 数组 - 图8

当想给一个函数传递数组时,命名数组以产生它的起始地址的事实相当重要。如果声明一个数组为函数参数,实际上真正声明的是一个指针。所以在下面的例子中,func1()和func2()有一样的参数表:

3.8.5 数组 - 图9

3.8.5 数组 - 图10

尽管func1()和func2()以不同的方式声明它们的参数[1],但是在函数内部的用法是一样的。这个例子暴露出了一些别的问题:数组不可以按值传递,也就是说,不会自动地得到传递给函数的数组的本地拷贝。因此,修改数组时,一直是在修改外部对象。如果想按照一般的参数那样提供按值传递,可能一开始会让人有点迷惑。

读者会注意到,print()对数组参数使用方括号语法。尽管把数组作为参数传递时,指针语法和方括号语法是一样的,但是方括号语法使得读者更清楚它的意思是把这个参数看做是一个数组。

还要注意,在每一种情况传递了参数size。仅仅传递数组的地址还不能提供足够的信息,必须知道在函数中的数组有多大,这样就不会超出数组的界。

数组可以是任何一种类型,包括指针数组。事实上,想给程序传递命令行参数时,C和C++的函数main()有特殊的参数表,其形式如:

3.8.5 数组 - 图11

第一个参数的值是第二个参数的数组元素个数。第二个参数总是char*数组,因为数组中的元素来自作为字符数组的命令行(记住,数组只能作为指针传递)。命令行中的每一个用空格分隔的字符串被转换成单独的数组参数。通过遍历数组,下面的程序可以打印出所有的命令行参数:

3.8.5 数组 - 图12

3.8.5 数组 - 图13

读者会注意到argv[0]是程序本身的路径和名字。它允许程序发现自己的信息。它也给程序参数数组增加一个或多个参数,所以一个常见的错误就是当想获取命令行参数argv[1]的值时,却去取argv[0]的值。

在函数main()中,不要强制使用argc和argv为标识符;这些标识符只是习惯用法(如果不使用它们,可能会让别人迷惑)。还有另一种声明argv的方式:

3.8.5 数组 - 图14

两种形式是等价的,但本书使用的版本更为直观,因为它直接表明“这是一个字符指针数组”。

从命令行中获得的是字符数组;如果想把数组看成是别的某种类型,应该在程序里负责转换它。为了便于转换为数值,在标准C库的<cstdlib>中声明了一些更有帮助的函数。最简单的是分别使用atoi()、atol()和atof()把ASCII字符数组转换为int、long和double浮点值。下面是一个使用atoi()的例子(另两个函数用同样的方式调用):

3.8.5 数组 - 图15

在这个程序中,可以在命令行中放置任意多个参数。读者会注意到for循环从值1开始,跳过了argv[0]中的程序名。如果在命令行上放置了一个包含小数点的浮点数,atoi()只取得小数点前面的数字部分。如果在命令行中没有数值,atoi()会返回零值。

3.8.5.2 探究浮点格式

本章已经介绍的printBinary()函数对于研究不同数据类型的内部结构是很合适的。最令人感兴趣的就是浮点格式,它允许C和C++在有限的空间里存储非常大和非常小的数。尽管在这里不能完全显示其细节,但是在float和double里的数字位被分为段:指数、尾数和符号位,它用科学计数法来存储数值。下面的程序允许打印出不同浮点数的二进制形式,所以读者可以自己推断出编译器浮点格式的使用方案(一般这是浮点数的IEEE标准,但是有的编译器可能不遵守)。

3.8.5 数组 - 图16

3.8.5 数组 - 图17

首先,程序通过检查argc的值保证给定了参数,如果有一个参数,则argc的值应该为2(如果没有参数,则为1,因为程序名总是argv的第一个元素)。如果程序失败了,会打印出一个消息并调用标准C的库函数exit()来终止程序。

程序从命令行中取得参数并使用函数atof()把字符转换成double浮点数。然后通过取得地址并把该数转换为一个unsigned char*指针作为一个字节数组。把其中的每一个字节传递给printBinary()显示出来。

我在自己的机器上通过了这个程序,打印字节时符号位出现在前面。有的机器可能和我的不一样,所以可能需要重新安排打印的方式。读者应认识到理解浮点格式并不是微不足道的。例如,一般不把指数和尾数以字节划分的边界存放,而是为每一部分保留若干位数,并把它们尽可能紧密地压缩进内存。要真的看看发生了什么,应该把数值的每一部分的大小找出来(符号位总是一位,而指数和尾数的位数的大小不同),并把每一部分的位数分别打印出来。

3.8.5.3 指针算术

如果用指针所做的工作只是把它看做是数组的一个别名,那么指向数组的指针可能不太令人感兴趣。但是,指针比这个更灵活,因为可以修改它们指向任何别的地方(但是记住,不能修改数组标识符来指向别的地方)。

指针算术(pointer arithmetic)指的是对指针的某些算术运算符的应用。指针算术是一个源自普通算术的单独主题,其原因在于为了正确运行,指针必须遵守特定的约束。例如,指针常用的运算符是++—“给指针加1”。它的实际意义是改变指针移向“下一个值”。下面是一个例子:

3.8.5 数组 - 图18

我的机器上的运行输出是:

3.8.5 数组 - 图19

3.8.5 数组 - 图20

这里令人感兴趣的是尽管对int和double进行的都是同样的操作“++”,但是对int只改变了4个字节,而对double改变了8个字节。当然并非总是这样,这取决于int和double浮点数的大小。这就是指针算术的技巧:编译器计算出指针改变的正确值,使它指向数组中的下一个元素(指针算术只有在数组中才是有意义的)。甚至在struct数组中也能这样工作:

我的机器上的运行结果是:

3.8.5 数组 - 图21

所以可以看到编译器对于struct(以及class和union)指针也能正确地工作。

指针算术运算也可以使用运算符“—”、“+”和“-”,但是后面两个运算符的使用是有限制的:不能把两个指针相加,如果使指针相减,其结果是两个指针之间相隔的元素个数。不过,一个指针可以加上或减去一个整数。下面是一个说明指针算术运算用法的例子:

3.8.5 数组 - 图22

3.8.5 数组 - 图23

这个程序以另一个宏开始,但是它使用了一个被称为字符串化的预处理器特征(在表达式前用一个‘#’实现),其作用是获得任何一个表达式并把它转换成为一个字符数组。这是很方便的,因为它允许打印一个表达式,后面接一个冒号,再接一个表达式的值。在main()中,可以看到这产生了一个有用的简化。

尽管++和—的前缀和后缀方式对指针来说都是有效的,但是在这个例子中只使用了前缀方式,因为在上面的表达式中指针间接引用之前先应用它们,所以它们允许看到运算的效果。注意只能加上和减去整数值,如果两个指针以这种方式结合,编译器是不允许的。

上面程序的输出是:

3.8.5 数组 - 图24

在各种情况下,指针算术根据所指元素的大小调整指针,使其指向“正确的地方”。

如果一开始指针算术运算看起来有点令人困扰,那么不必担心。大多数情况下只需要创建数组和用[]表示的数组下标,一般所需要的最为复杂的指针算术运算是++和—。指针运算一般都用于更为灵活和复杂的程序中,标准C++库中许多容器隐藏了大多数的灵活细节,所以不必担心这一点。

[1]除非采取了严格的办法:“在C/C++中的所有参数是通过值传递的,数组的‘值’是由数组标识符产生的:它是一个地址。”从汇编语言的观点来看这可能是真的,但是当用更高层的概念工作时,我认为这是没有帮助的。在C++中附加的引用生成“所有的传递都是通过值”的说法更会使人混淆,对于这一点我认为按照与“以地址传递”相对的“以值传递”来思考更好。