3.5.1 定义和初始化内置数组
数组是一种复合类型(参见2.3节,第50页)。数组的声明形如a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式(参见2.4.4节,第65页):
- unsigned cnt = 42; // 不是常量表达式
- constexpr unsigned sz = 42; // 常量表达式,关于constexpr,参见2.4.4节(第66页)
- int arr[10]; // 含有10个整数的数组
- int *parr[sz]; // 含有42个整型指针的数组
- string bad[cnt]; // 错误:cnt不是常量表达式
- string strs[get_size()]; // 当get_size是constexpr时正确;否则错误
默认情况下,数组的元素被默认初始化(参见2.2.1节,第43页)。
和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。
定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。
显式初始化数组元素
可以对数组的元素进行列表初始化(参见3.3.1节,第98页),此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值(参见3.3.1节,第98页):
- const unsigned sz = 3;
- int ia1[sz] = {0, 1, 2}; // 含有3个元素的数组,元素值分别是0, 1, 2
- int a2[] = {0, 1, 2}; // 维度是3的数组
- int a3[5] = {0, 1, 2}; //等价于a3[] = {0, 1, 2, 0, 0}
- string a4[3] = {"hi", "bye"}; // 等价于 a4[] = {"hi", "bye", ""}
- int a5[2] = {0,1,2}; // 错误:初始值过多
字符数组的特殊性
字符数组有一种额外的初始化形式,我们可以用字符串字面值(参见2.1.3节,第39页)对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
- char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
- char a2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显式的空字符
- char a3[] = "C++(www.cppentry.com)"; // 自动添加表示字符串结束的空字符
- const char a4[6] = "Daniel"; // 错误:没有空间可存放空字符!
a1的维度是3,a2和a3的维度都是4,a4的定义是错误的。尽管字符串字面值"Daniel"看起来只有6个字符,但是数组的大小必须至少是7,其中6个位置存放字面值的内容,另外1个存放结尾处的空字符。
不允许拷贝和赋值
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
- int a[] = {0, 1, 2}; // 含有3个整数的数组
- int a2[] = a; // 错误:不允许使用一个数组初始化另一个数组
- aa2 = a; // 错误:不能把一个数组直接赋值给另一个数组
一些编译器支持数组的赋值,这就是所谓的编译器扩展(compiler extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常工作。
理解复杂的数组声明
和vector一样,数组能存放大多数类型的对象。例如,可以定义一个存放指针的数组。又因为数组本身就是对象,所以允许定义数组的指针及数组的引用。在这几种情况中,定义存放指针的数组比较简单和直接,但是定义数组的指针或数组的引用就稍微复杂一点了:
- int *ptrs[10]; // ptrs是含有10个整型指针的数组
- int &refs[10] = /* */; // 错误:不存在引用的数组
- int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
- int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
默认情况下,类型修饰符从右向左依次绑定。对于ptrs来说,从右向左(参见2.3.3节,第58页)理解其含义比较简单:首先知道我们定义的是一个大小为10的数组,它的名字是ptrs,然后知道数组中存放的是指向int的指针。
但是对于Parray来说,从右向左理解就不太合理了。因为数组的维度是紧跟着被声明的名字的,所以就数组而言,由内向外阅读要比从右向左好多了。由内向外的顺序可帮助我们更好地理解Parray的含义:首先是圆括号括起来的部分,*Parray意味着Parray是个指针,接下来观察右边,可知道Parray是个指向大小为10的数组的指针,最后观察左边,知道数组中的元素是int。这样最终的含义就明白无误了,Parray是一个指针,它指向一个int数组,数组中包含10个元素。同理,(&arrRef)表示arrRef是一个引用,它引用的对象是一个大小为10的数组,数组中元素的类型是int。
当然,对修饰符的数量并没有特殊限制:
- int *(&arry)[10] = ptrs; // arry是数组的引用,该数组含有10个指针
按照由内向外的顺序阅读上述语句,首先知道arry是一个引用,然后观察右边知道,arry引用的对象是一个大小为10的数组,最后观察左边知道,数组的元素类型是指向int的指针。这样,arry就是一个含有10个int型指针的数组的引用。
要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。
3.5.1节练习
练习3.27:假设txt_size是一个无参数的函数,它的返回值是int。请回答下列哪个定义是非法的?为什么?
unsigned buf_size = 1024;
(a) int ia[buf_size]; (b) int ia[4 * 7 - 14];
(c) int ia[txt_size()]; (d) char st[11] = "fundamental";
练习3.28:下列数组中元素的值是什么?
- string sa[10];
- int ia[10];
- int main() {
- string sa2[10];
- int ia2[10];
练习3.29:相比于vector来说,数组有哪些缺点,请列举一些。