1.4 数组
简介
数组是类似标准库vector
的数据结构,但是在性能和灵活性的权衡上与vector
有所不同。与vector
相似的是,数组也是存放类型相同对象的容器;不同的是数组的大小确定不变,不能向数组中随意地添加元素。因为数组的大小固定,因此对于某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。
Tips:如果不清楚元素的确切个数,请使用
vector
。
定义和初始化
1. 数组的维度必须是常量表达式[存疑]
Tips:数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的,即数组的维度必须是一个常量表达式。
unsigned i = 10;
constexpr unsigned j = 5;
int arr1[10];
int arr2[i]; // TODO: 按照C++Primer的说法会报错(因为i不是常量表达式), 但是C++11正常编译过了, 但是最好不要用非常量表达式来指明数组的维度
int arr3[j];
2. 默认初始化
和内置类型变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。
3. 显式初始化
我们可以对数组进行列表初始化,此时允许忽略数组的维度,编译器会根据初始值的数量推断出来。如果指明了维度,那么初始值的总数量不应该超出指定的大小,如果维度比提供的初始值数量大,那么用提供的初始值初始化靠前的元素,剩下的元素被初始化为默认值。
int arr1[3] = {0, 1, 2}; // 含有三个元素的数组, 元素值分别是0, 1, 2
int arr2[] = {0, 1, 2}; // 含有三个元素的数组, 元素值分别是0, 1, 2
int arr3[4] = {0, 1}; // 含有四个元素的数组, 元素值分别是0, 1, 0, 0
int arr4[2] = {0, 1, 2}; // 编译报错: too many initializers for ‘int [2]’
4. 字符数组的特殊初始化
我们可以用字符串字面值初始化字符数组,这时候字符串字面值结尾处空字符也会被拷贝到字符数组中:
char a1[] = {'C', 'A', 'T'}; // 列表初始化, 没有空字符
char a2[] = {'C', 'A', 'T', '\0'}; // 列表初始化: 含有显式的空字符
char a3[] = "CAT"; // 字符串字面值初始化: 维度为4, 会自动添加空字符在数组末尾
不允许拷贝和赋值
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
int a1[] = {0, 1, 2};
int a2[] = a1; // 编译报错: array must be initialized with a brace-enclosed initializer
a2 = a1; // 编译报错: invalid array assignment
遍历数组元素
Tips:在使用数组下标时,通常将其定义为
size_t
类型(它是定义在cstddef
头文件中一种机器相关的无符号类型,被设计得足够大以便能表示内存中任意对象的大小)。
与vector和string类型一样,当需要遍历数组的所有元素时,最好的办法也是使用范围for语句:
int arr[5] = {0, 1, 2, 3, 4};
for (auto i : arr) {
cout << i << " ";
}
cout << endl;
数组和指针
1. 数组名字会被当做指向首元素的指针
Tips:C++语言中,使用数组名字的时候编译器一般会将它转化成指向数组首元素的指针。(数组作为
decltype
关键字参数、取地址符&
、sizeof
和typeid
等运算符的运算对象时,上述自动转换不会发生)
int arr[] = {0, 1, 2};
int *p1 = arr; // p1是指向数组arr首元素的指针, 等价于 p1 = &arr[0];
auto p2(p1); // p2也是指向数组arr首元素的指针
但是使用decltype
关键字时不会自动发生数组名字到数组首元素指针的转换:
// decltype(arr)返回的类型是3个int构造的数组
int arr[] = {0, 1, 2};
decltype(arr) arr2 = {3, 4, 5};
2. 利用指针遍历数组
就像使用迭代器vector
对象中的元素一样,使用指针也能遍历数组中的元素,不过我们要设法取得指向数组首元素和尾后元素的指针:
#include <iostream>
// 当然这是一种奇怪的写法, 最好还是用范围for循环去遍历元素
int main() {
int arr[] = {0, 1, 2};
int *e = &arr[3]; // 指向数组尾后元素的指针
for (int *b = arr; b != e; ++b) {
std::cout << *b << std::endl;
}
}
上面这种写法虽然能得到尾后指针,但是极容易出错。C++11新标准引入了两个名为begin()
和end()
的函数,用于获取数组的首元素指针和尾后指针。
Tips:注意尾后指针不能解引用和递增操作。
#include <iostream>
int main() {
int arr[] = {0, 1, 2};
for (int *b = std::begin(arr), *e = std::end(arr); b != e; ++b) {
std::cout << *b << std::endl;
}
}
3. 数组的指针运算
指向数组元素的指针可以执行迭代器运算,比如解引用、递增递减、比较、与整数相加减和两个指针相减等,用在指针和用在迭代器上的意义完全一致。
#include <iostream>
int main() {
int arr[] = {0, 1, 2, 3, 4};
int *p0 = arr; // p0指向arr首元素
int *p1 = p0 + 1; // p1指向arr第二个元素
auto n = std::end(arr) - std::begin(arr); // 数组长度, 类型是std::ptrdiff_t
std::cout << *p0 << std::endl; // 输出0
std::cout << *p1 << std::endl; // 输出1
std::cout << n << std::endl; // 输出5
}
4. 数组指针与下标
Tips:只要指针指向的是数组中元素(或者尾后元素)都可以执行下标运算,另外注意内置的下标运算符所用的索引值不是无符号类型(可以为负),这点与
vector
和string
不同。
前面提到编译器会自动执行数组名字到数组首元素指针的转换,当对数组使用下标运算的时候也不例外:
#include <iostream>
int main() {
int arr[] = {0, 1, 2, 3, 4};
int *p = arr; // p是指向数组首元素的指针
std::cout << p[3] << std::endl; // 可以直接对数组指针使用下标运算, 输出3
int *q = &arr[2]; // p是指向数组第三个元素的指针
std::cout << q[1] << std::endl; // 等价于*(q + 1), 输出3
std::cout << q[-2] << std::endl; // 下标可以为负, 等价于*(q - 2), 输出0
}
多维数组
1. 简介
严格来说,C++中并没有多维数组,所谓的多维数组仅仅是数组的数组。对于二维数组而言,通常把第一个维度称为行,第二个维度称为列。
2. 定义与初始化
显式指明多维数组元素时一般使用列表初始化:
// 三个元素的数组, 每个元素都是大小为4的数组
int ia[3][4] = {
{0, 1, 2, 3}, // 第一行初始值
{4, 5, 6, 7}, // 第二行初始值
{8, 9 , 10, 11} // 第三行初始值
};
// 没有标识每行的花括号, 和上面等价
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 显式地初始化每行的首元素
int ia[3][4] = {{0}, {4}, {8}};
// 显式地初始化第一行, 其他元素被初始化为0
int ia[3][4] = {0, 1, 2, 3};
// 将所有元素初始化为0
int ia[3][4] = {0};
3. 遍历多维数组
以二维数组为例,最简单的是直接使用两层嵌套的for循环:
#include <iostream>
int main() {
const size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt] = {
{0, 1, 2, 3}, // 第一行初始值
{4, 5, 6, 7}, // 第二行初始值
{8, 9 , 10, 11} // 第三行初始值
};
for (size_t i = 0; i != rowCnt; ++i) {
for (size_t j = 0; j != colCnt; ++j) {
std::cout << ia[i][j] << std::endl;
}
}
}
C++11新标准允许我们使用范围for循环简化对多维数组元素的遍历:
#include <iostream>
int main() {
const size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt] = {
{0, 1, 2, 3}, // 第一行初始值
{4, 5, 6, 7}, // 第二行初始值
{8, 9 , 10, 11} // 第三行初始值
};
for (const auto &row : ia) {
for (int elem : row) {
std::cout << elem << std::endl;
}
}
}
Tips:要使用范围for循环遍历多维数组时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型,这是为了避免数组被自动转换为指针。
如果我们在范围for循环中不使用引用类型,编译器会报错:
#include <iostream>
int main() {
const size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt] = {
{0, 1, 2, 3}, // 第一行初始值
{4, 5, 6, 7}, // 第二行初始值
{8, 9 , 10, 11} // 第三行初始值
};
for (auto row : ia) {
// row会被编译器自动推断为int*类型, 因此这里编译会报错: no matching function for call to ‘end(int*&)’
for (int elem : row) {
std::cout << elem << std::endl;
}
}
}
当然我们也可以用标准库函数begin()
和end()
来遍历数组元素:
#include <iostream>
int main() {
const size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt] = {
{0, 1, 2, 3}, // 第一行初始值
{4, 5, 6, 7}, // 第二行初始值
{8, 9 , 10, 11} // 第三行初始值
};
for (auto p = std::begin(ia); p != std::end(ia); ++p) {
for (auto q = std::begin(*p); q != std::end(*p); ++q) {
std::cout << *q << std::endl;
}
}
}
使用标准库类型替代旧代码中的数组
C++中推荐使用vector
和array
标准库类型代替数组,但是又不得不与那些充满了数组的旧代码衔接。前面介绍过不允许使用数组初始化另一个内置类型的数组,但是我们可以使用数组来初始化vector
对象:
int ia[] = {0, 1, 2, 3, 4};
// 拷贝所有元素
std::vector<int> iv(std::begin(ia), std::end(ia));
// 拷贝三个元素: ia[1], ia[2], ia[3]
std::vector<int> iv(ia + 1, ia + 4);