数组的定义和初始化
数组的大小
首先来看下边这段代码。请问它有问题吗?
答案是有的。因为 n 是一个变量但不是常量,所以它不能用作数组 a 的长度。(如果读者对此感到困惑:明明 n 初始化为 5 了啊,它的值应该能确定啊?事实上不是这样的,变量的初始化在运行时才会执行,而数组的大小要求在编译期间就能得到。编译器没办法“提前感知”这个大小。)所以如果编译器足够严格的话,会报类似 ISO C++ forbids variable length array 的错误。那么这样呢?
这样也是不行的。因为变量 m 仅仅是只读变量,但它不是常量(非常量初始化)。所以我们也不能这样写。正确的做法是使用常量作为数组大小。最简单的做法是只用字面量(如 int a[5]{};),但是这样的话会有一些小麻烦。在某些场合中,我们可能声明并定义了一堆相同长度的数组:
这个时候若想同时修改它们的长度,就需要进行多次改动。如果用 constexpr 变量来作为数组长度:
则只需修改一次 N 的值就能实现批量修改数组大小。回到最初的代码,这是我更推荐的写法。
C++ 对数组大小是常量有着严格的限制,但是 C 语言没有这样的要求。因此许多编译器也允许那些不标准的用法。如果你使用 GCC 编译器的话,我建议将
-pedantic或-pedantic-errors(可理解为严格模式)开关启用以防止这种错误的用法。
除此之外,很显然地,数组的大小必须是正数(意味着不存在拥有零个元素的数组)。
C++ 要求数组的大小求值后可隐式转换为
std::size_t类型。在某些编译器下,这个类型与unsigned long long等价。
初始化器
最容易理解的初始化器长成这样:
int a[5]{1, 2, 3, 4, 5};
对于大小为 5 的数组,初始化器内恰好有 5 个值,分别对应 a[0] a[1] a[2] a[3] a[4]。其次是比元素个数少的初始化器,比如:
int a[5]{1, 2, 3};
这个时候,a[0] a[1] 和 a[2] 分别被初始化为 1 2 3,然后剩余的元素采用零初始化(Zero initialization)——也就是初始化为零值。因此这段代码
的输出结果为 1 2 3 0 0。
如果初始化器内留空,那么所有元素都会采用零初始化。这段代码
的输出为 0 0 0 0 0。
严格说,剩余的元素采用的是“值初始化”(Value initialization)。非类类型的值初始化就是零初始化(因此正文使用了“零初始化”来更形象地描述);而类类型的值初始化则是默认初始化。
但是,初始化器内部的值个数不能超过数组大小,否则导致编译错误:
最后还有一种初始化方法。它写成这样:
int a[]{1, 2, 3, 4, 5, 6};
这个时候,a 的大小留空;此时数组 a 的大小则根据初始化器中值的个数自动推导。比如这里 a 被推导为拥有 6 个 int 类型元素的数组,即 int[6],因为初始化器中提供了 6 个值。这是一种简略的写法,在某些情形下比较有用。
省略数组大小的前提是必须拥有初始化器,否则编译错误:不含初始化器且未指明大小的数组类型属于不完整类型;不完整类型的对象只能声明而不能定义。这种不完整类型称为“未知边界数组”,它在 C 语言里有一定用处,但是在 C++ 里经常招致麻烦。
关于类型标识
你应当知道对于一般的变量来说,如下声明语句
int a, b;
会引入两个名为 a b 的 int 类型变量。但是对于数组来说,
int a, b[10];
并不是将 a 和 b 都声明并定义为数组。实际上,这里 a 仍然是一个 int 型变量,只有 b 是一个 int[10] 数组。
所以道理就是,在一行声明语句声明多个名字时,只有中括号贴着的那个名字才能成为数组。比如
int a[10], b;
中,a 是类型为 int[10] 的数组,而 b 只是一个 int。如果想要让两者都成为数组,必须这样写:
int a[10], b[10];
嗯,别忘了加上初始化器,否则里面的东西是不确定的:
int a[10]{}, b[10]{};
借此机会,我们正式地介绍一下类型标识(Type ID)这个概念。类型标识是指明一个类型的文本,比如 int 指明了一个普通整型,double 指明一个双精度浮点型,bool 指明一个布尔类型。与类型说明符不同,数组、函数也拥有类型标识(但它们没有默认的类型说明符)。比如 int[10] 就是拥有 10 个 int 的数组的类型标识,而 bool(int) 是一个接受一个 int 参数,返回值类型为 bool 的函数的类型标识。获得一个变量的类型标识很简单,直接把所有变量名从声明中拿走就是了。比如:
第一个声明语句中,把 result 拿走剩下的 long long 就是 result 的类型标识(这和类型说明符一致);把 a 拿走得到的 char[42] 就是 a 的类型标识;把 f 拿走剩下的 int(int, bool) 就是 f 的类型标识。
类型标识可以用在类型转换运算符和 sizeof 运算符中。比如下面的例子:
当 int 类型占 4 个字节的时候,int[5] 这个数组类型就占 20 字节。因为数组类型中每一个元素都是连续、紧密地存储的:
