数组

数组类似于vector,但提供了性能和灵活性的 权衡。不同的是,数组有固定大小;我们不能向数组 添加元素。

定义和初始化数组

数组是一种复合类型(compound type)。数组声明的形式为a[d],其中a是定义的名字,d是数组的大小(dimension)。 因为数组中元素的个数是数组类型的一部分。因此,大小必须在编译期间就知道,这意味这它必须是个常量表达式(constant expression)。

unsigned cnt = 42;
constexpr unsigned sz = 42;

int arr[10]; // ok
int *parr[sz]; // ok
string bad[cnt]; // error
string strs[get_size()]; // ok if get_size is constexpr, error otherwise

默认地,数组里的元素都是默认初始化(default initialized)的。

就像内置类型的变量,定义在函数内的内置类型的数组也是未定义的值。

数组的大小其实是std::size_t类型,也是依赖机器的无符号数。定义在#include <cstddef>

显式初始化数组元素

我们可以使用列表初始化数组的元素;这样的话,我们可以省略数组大小。我们指定大小,初始化器的数目不能超过指定的大小; 如果指定的大小大于初始化器数目,那其它值就是value initialized。

const unsigned sz = 3;
int ia1[sz] = {0, 1, 2};
int a2[] = {0, 1, 2}; 
int a3[5] = {0, 1, 2}; // same as {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; // same as {"hi", "bye", ""}
int a5[2] = {0, 1, 2}; // error

特殊的字符数组

字符数组还支持一种特殊的初始化方式:通过字面字符串常量。注意使用这种方式初始化时,会以一个空字符结尾。

char a1[] = {'C', '+', '+', '\0'}; // 显式的有个空字符
char a2[] = "C++"; // 自动加上一个空字符
const char a3[6] = "Daniel"; // error: 没有空间给最后一个空字符

不能拷贝,不能赋值

我们不能通过另一个数组的拷贝初始化数组,也不能对使用数组赋值

int a[] = {0, 1, 2}; 
int a2[] = a; // error:不能使用另一个数组初始化一个数组
a2 = a; // error:不能另一个数组对数组赋值

但是,有的编译器允许对数组的赋值,但不推荐违反标准的用法。

理解复杂的数组声明

可以定义指针数组。因为数组本身是对象,我们可以定义指向数组的指针和引用。

int *ptrs[10]; // ptrs is an array of ten pointers to int
int &refs[10] = /* ? */; // error: no arrays of references
int (*Parray)[10] = &arr; // Parray points to an array of ten ints
int (&arrRef)[10] = arr; // arrRef refers to an array of ten ints

默认的类型修饰符是从右到左绑定的。比如ptrs,我们先看到的是大小为10的数组,名字为ptrs, 容纳的是指向int的指针(holds pointers to int)。

而对于Parray,从右到左读是不合适的。这时更好的理解方式:从数组的名字开始,从内到外读。 于是,先看到*Parray,表示Parray是一个指针。看右边,表明Parray指向的是一个大小为10的 数组;看左边,表明数组的元素是int。

指针与数组

C++的数组和指针关系密切。当使用数组时,编译器会把它转成一个指针。 即在我们使用数组的大多数地方,编译器自动将其替代成指向第一个元素的指针。

string nums[] = {"one", "two", "three"};
string *p = nums; // 等价于 &nums[0]

这意味着,我们对数组的操作实际是对指针的。

int ia = {0, 1, 2, 3};
auto ia2(ia); // ia2 is an int*, points to the first element in ia

但是,使用decltype时,返回的类型还是数组,不是指针。

decltype(ia) ia3 = {0, 1, 2, 3};
ia3 = p; // error: cannot assign an int* to an array

指针是迭代器

指向数组元素的指针支持vector和string迭代器操作。比如,可以对其自增:

int arr[] = {0, 1, 2, 3, 4};
int *p = arr;
++p; // p points to arr[1]

我们也可以得到类似的off-the-end指针,int *e = &arr[10]

begin和end函数

像上面手动得到的off-the-end,很容器犯错。为了更安全地使用指针,C++ 11引入了两个函数: beginend;就像容器的成员。但注意数组不是类类型,这样函数不是成员函数。

int ia[] = {0, 1, 2, 3, 4, 5};
int *beg = begin(ia); // 指向第一个元素
int *last = end(ia); // 指向最后一个元素的下一个位置

指针的算术

解引用,自增,比较,加上一个整数,两个指针的相减,这些之前迭代器的特殊操作这里都支持。

constexpr size_t sz = 5;
int arr[sz] = {1, 2, 3, 4};

auto n = end(arr) - being(arr);

指针类型的相减的结果是ptrdiff_t类型,和size_t类型,也是一个依赖机器的有符号类型。

指针的解引用和指针算术

int ia[] = {0, 2, 4, 6, 8};
int last = *(ia + 4); // 将last初始化为8
last = *ia + 4; // 相当于ia[0] + 4

指针与下标

只要指针指向的是数组中的元素(包括最后一个元素的下一个),我们就可以对其使用下标。

int *p = &ia[2]; // p指向第三个
int j = p[1]; // 相当于*(p + 1),即ia[3]
int k = p[-2]; // ia[0]

需要注意的是,vectorstring的下标都是无符号的,而这里内置的下标操作的索引可以为负。