预计阅读本页时间:-
存储类解释
和其他变量相似,数组可以被定义为多种存储类(storage class),第12章将详述此主题。目前,只需要了解本章的数组属于自动存储类。也就是说,数组是在一个函数内声明的,并且声明时没有使用关键字static。到目前为止,本书所用的变量和数组都是自动类型的。
现在提起存储类的原因是:不同存储类有时具有不同的属性,因此不能把本章的知识推广到其他存储类。例如,如果没有进行初始化,一些存储类的变量和数组会把它们的存储单元设置为0。
初始化列表中的元素数目应该和数组大小一致。如果二者不一致,会出现什么情况?我们仍然使用前面那个例子,如程序清单10.3所示,其中的初始化列表中缺少两个数组元素。
程序清单10.3 somedata.c程序
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
这次的输出结果如下:
从上面结果我们可以知道,编译器做得很好。当数值数目少于数组元素数目时,多余的数组元素被初始化为0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中存储的是无用的数值;但是如果部分初始化数组,未初始化的元素则被设置为0。
如果初始化列表中项目的个数大于数组大小,编译器会毫不留情地认为这是一个错误。然而,可以采用另外一种形式以避免受到编译器的此类奚落:您可以省略括号中的数字,从而让编译器自动匹配数组大小和初始化列表中的项目数目(请参见程序清单10.4)。
程序清单10.4 day_mon2.c程序
程序清单10.4中有两点需要注意:
● 当使用空的方括号对数组进行初始化时,编译器会根据列表中的数值数目来确定数组大小。
● 注意for循环的控制语句。由于人工计算容易出错,因此可以让计算机来计算数组的大小。运算符sizeof给出其后的对象或类型的大小(以字节为单位)。因此sizeof days是整个数组的大小(以字节为单位),sizeof days[0]是一个元素的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组中元素的数目。
该程序运行结果如下:
意外!我们只向数组内放入了10个数值,但是我们的想法是让程序自动找到数组的大小以免我们试图向数组填入过多的元素。这暴露出自动计数的弊端:初始化的元素个数有误时,我们可能意识不到。
另外,还有一个很简短的初始化数组的方法,这种方法仅限于字符串,将在下一章中详述。
10.1.2 指定初始化项目(C99)
C99增加了一种新特性:指定初始化项目(designated initializer)。此特性允许选择对某些元素进行初始化。例如:要对数组的最后一个元素初始化。按照传统的C初始化语法,需要对每一个元素都初始化之后,才可以对最后的元素进行初始化:
而C99规定,在初始化列表中使用带有方括号的元素下标可以指定某个特定的元素:
对于通常的初始化,在初始化一个或多个元素后,未经初始化的元素都将被设置为0。程序清单10.5中是一个较为复杂的例子。
程序清单10.5 designate.c程序
如果编译器支持C99特性,则输出结果如下:
从输出结果可以看出指定初始化项目有两个重要特性。第一,如果在一个指定初始化项目后跟有不止一个值,例如在序列[4]=31,30,31中这样,则这些数值将用来对后续的数组元素初始化。也就是说,把31赋给days[4]之后,接着把30和31分别赋给days[5]和days[6]。第二,如果多次对一个元素进行初始化,则最后的一次有效。例如,在程序清单10.5中,前面把days[1]初始化为28,而后面的指定初始化[1]=29覆盖了前面的数值,于是days[1]的数值最终为29。
10.1.3 为数组赋值
声明完数组后,可以借助数组的索引(即下标)对数组成员进行赋值。例如,以下程序段的功能是把一些偶数赋给数组:
注意这种赋值的方式是使用循环对元素逐个赋值。C不支持把数组作为一个整体来进行赋值,也不支持用花括号括起来的列表形式进行赋值(初始化的时候除外)。下面这段代码展示了一些不允许的赋值方式:
10.1.4 数组边界
使用数组的时候,需要注意数组索引不能超过数组的边界。也就是说,数组索引应该具有对于数组来说有效的值。例如,假定您有这样的声明:
那么您在使用数组索引的时候,要确保它的范围在0和19之间,因为编译器不会为您检查出这种错误。
考虑程序清单10.6中的程序。它创建了一个包含4个元素的数组,但却不小心使用了从–1到6的索引值。
程序清单10.6 bounds.c程序
编译器不检查索引的合法性。在标准C中,如果使用了错误的索引,程序执行结果是不可知的。也就是,程序也许能够运行,但是运行结果可能很奇怪,也可能会异常中断程序的执行。我们使用Digtal Mars 8.4运行程序,其输出结果如下:
注意我们使用的编译器看起来是把value2正好存储在数组后面的那个存储单元中,把value1存储在数组前面的那个存储单元中(其他的编译器可能采取不同的顺序在内存中存储数据)。这样arr[-1]就和valuel对应同一个存储单元,arr[4]和value2对应同一个存储单元。因此,使用超出数组边界的索引会改变其他变量的数值。对于不同的编译器,输出结果可能不同。
也许您会产生疑问,为什么C会允许这种事情发生。这仍然是出于C信任程序员的原则。不检查边界能够让C程序的运行速度更快。在程序运行之前,索引的值有可能尚未确定下来,所以编译器此时不能找出所有的索引错误。为保证程序的正确性,编译器必须在运行时添加检查每个索引是否合法的代码,这会导致程序的运行速度减慢。因此,C相信程序员的代码是正确的,从而可以得到速度更快的程序。但是并不是所有程序员都能够完美地做到这一点,因此问题就产生了。
一件需要记住的简单的事情就是,数组的计数是从0开始的。避免出现这个问题比较简单的方法是:在数组声明中使用符号常量,然后程序中需要使用数组大小的地方都直接引用符号常量:
这样做的好处是保证整个程序中数组大小始终一致。
10.1.5 指定数组大小
在前面提到的例子中,我们声明数组时使用的是整数常量:
还允许使用什么?直到C99标准出现之前,声明数组时在方括号内只能使用整数常量表达式。整数常量表达式是由整数常量组成的表达式。sizeof表达式被认为是一个整数常量,而(和C++不一样)一个const值却不是整数常量。并且该表达式的值必须大于0:
请参看上面的注释,遵循C90标准的C编译器不允许最后两个声明。而C99标准允许这两个声明,但这创建了一种新数组,称为变长数组(variable-length array),简称VLA。
C99引入变长数组主要是为了使C更适于做数值计算。例如,VLA的引入简化了将FORTRAN语言的数值运算例程库转换为C代码的过程。VLA有某些限制;例如,声明时不能进行初始化。在充分了解古典C数组的局限性之后,我们将在本章的后面详细介绍VLA。