0. 数组的概念
参考【深度C++】之“类型与变量”我们知道,数组是一种复合类型。
数组是一种存放多个类型相同的对象的容器。对象本身没有名字,通过其所在数组中的位置访问。
我们来理解一下定义:
- 类型相同,意味着不可以一会儿是int,一会儿是double
- 对象,意味着引用类型不可以放入数组,但是指针可以
- 位置,意味着访问数组的方式
下面就是一个数组:
int arr[10]; // 含有10个整数的数组
关于数组相关的内容,我们需要知道:
- 数组的声明
- 数组的初始化
- 拷贝和赋值数组
- 访问数组的元素
- 理解字符数组
- 数组与指针
- 数组与引用
- 多维数组(数组与数组)
- 数组的遍历(4种方式)
1. 数组的声明
前例:
int arr[10]; // 含有10个整数的数组
中括号中的内容是数组的维度,维度必须是编译时已知的,因此维度必须是一个常量表达式。
2. 数组的初始化
默认情况下,数组的元素执行默认初始化,即有可能出现未定义的值。
可以使用初始值列表进行数组的初始化,此时可以省略数组的维度。
const unsigned sz = 3; // 常量
// 含有三个元素的数组,元素值是0,1,2
int ial[sz] = {0, 1, 2};
// 可以省略不写
int a2[] = {0, 1, 2};
// 初始值列表的数量可以小于数组维度
// 此时剩余元素进行值初始化。
int a3[5] = {0, 1, 2};
上述示例需要注意,当初始值列表中元素数量少于数组的维度时,剩余元素进行值初始化。
3. 拷贝和赋值数组
不允许拷贝和赋值数组。
int a[] = {1, 2, 3};
int a2[] = a; // ERROR!
int a3[] = {4, 5, 6};
a = a3; // ERROR!
4. 访问数组的元素
通过数组的概念,我们了解到通过位置来访问数组元素。
位置通常也被称作索引、下标。
数组索引从0开始,其类型为size_t类型,不必是一个常量表达式。
int a[] = {1, 2, 3};
cout << a[0]; // 输出1
size_t i = 1;
cout << a[1]; // 输出2
最好再访问数组的时候,检查下标是否在合理的范围之内。如果你确定不会超越数组范围,则可以不用检查。
如上述示例,我若访问a[4]
,这显然是有问题的,但是编译器不会帮你检查。
5. 理解字符数组
在数组的知识领域里,字符数组是比较特殊的一类数组。
char s[] = {'h', 'e', 'l', 'l', 'o', '\0'};
我们可以使用字符串字面值初始化数组:
char s[] = "Hello";
注意,此时数组的维度是6,字符串会默认添加一个空字符\0
cout << sizeof(s) / sizeof(char); // 输出6
要想正确的使用字符数组,一定要时刻考虑'\0'
,不论是定义还是使用。
6. 数组与指针
因为指针是对象,所以可以放入数组…
(╯‵□′)╯︵┻━┻
6.1 包含指针的数组和指向数组的指针
int arr[10];
// 包含10个整型指针的数组
int *ptrs[10];
// 指向包含10个整数的数组的指针
int (*ptr_arr)[10] = &arr;
6.2 先右后左,由内向外
怎么理解上面的定义?答:用计算机的思维。
先看ptrs
。①从变量名ptrs
开始,先右后左,先遇到中括号[]
,此时判定它是一个数组。②是一个什么样的数组呢?不能往右了,我们向左,看到了*
,此时判定它是一个包含10个指针的数组。③什么样的指针呢?继续往左,看到了int
,此时判定它是一个包含10个整型指针的数组。
再看ptr_arr
。①从变量名开始,先右后左,不能右了,向左,看到了*
,此时判定它是一个指针。②是一个什么样的指针呢?由内向外,跳出括号,先右后左,看到了[]
,此时判定它是一个指向数组的指针。③什么样的数组呢?不能往右了,我们向左,看到了int,此时判定它是一个指向包含10个整数的数组的指针。
这里比较复杂的原因是复合类型的复合类型。判定一个复合类型需要一个基础类型,因此会有很多定语修饰,这也是我们每走一步就提问“什么样”的原因。
6.3 编译器会将数组转换成指针
除了我们在定义数组和指针上,二者有交集,在使用数组时,指针和数组也不分家。
因为编译器一般会把数组转换成指向首元素的指针。
int arr[] = {1, 2, 3};
cout << *arr; // 输出1
上面的代码利用真正的指针实现:
int arr[] = {1, 2, 3};
int *p = &arr[0];
cout << *p; // 输出1
这样,我们就可以做一些特殊的操作了。
int arr[] = {1, 2, 3};
auto arr2(arr); // arr2的类型,是指针
cout << sizeof(arr2); // 输出8
cout << *(arr2 + 2); // 输出3
7. 数组与引用
引用不是对象,所以没有包含引用的数组,只有数组的引用:
int arr[10];
int (&r_arr)[10] = arr;
理解这个的方法,还是“先右后左,由内向外”。
那聪明的你告诉我,这个是啥子:
int *(&arry)[10] = ptrs;
8. 多维数组(数组与数组)
C++中并没有多维数组的概念,所谓的多维数组,不过是数组的数组(自己和自己复合)。
定义多维数组,需要多个维度。
// mat是一个数组,维度为3,它的内容是
// 另一类数组,维度是3的整形数组。
int mat[3][3];
依旧可以使用初始值列表进行初始化:
int mat0[3][3] = {{1, 2, 3},
{2, 3, 6},
{9, 3, 2}};
// 可以省略维度的具体信息
int mat2[3][3] = {1, 2, 3, 2, 3, 6, 9, 3, 2};
// 这样初始化每行的首元素,其余元素值初始化
int mat3[3][3] = {{1}, {2}, {9}};
// 注意和上面不等价,此处初始化了第一行的元素
int mat3[3][3] = {1, 2, 9};
9. 数组的遍历
对数组说,最常见的操作就是遍历。
9.1 通过下标遍历
const int sz = 10;
int arr[sz] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int i = 0; i < sz; ++i) {
cout << arr[i];
}
这种方法得提前知道数组的维度,不然容易出现越界。我们可以通过sizeof(arr) / sizeof(int)
得到。
int arr[] = {0, 1, 2, 3, 4};
const int sz = sizeof(arr) / sizeof(int);
for (int i = 0; i < sz; ++i) {
cout << arr[i];
}
9.2 范围for语句
int mat[3][3] = {{1, 2, 3},
{2, 3, 6},
{9, 3, 2}};
for (auto &row : mat) {
for (auto el : row) {
cout << el;
}
cout << endl;
}
注意:范围for语句除了最内层的for,其余的外层for必须使用引用。否则会将row转换为每行的首元素的地址,编译无法通过。
9.3 指针视角
int mat[3][3] = {{1, 2, 3},
{2, 3, 6},
{9, 3, 2}};
for (int (*row)[3] = mat; row != mat + 3; ++row) {
for (int *col = *row; col != *row + 3; ++col) {
cout << *col;
}
cout << endl;
}
row
是一个指向包含三个整形元素数组的指针,解引用指针row
将得到包含三个整形元素的数组,但是数组和指针存在天然等价关系,因此*row
就是指向包含三个整形元素的数组的首地址,也是一个类型为int *
的指针,可以赋值给*col
,通过col去访问。
这里的mat + 3
也是通过指针的视角去看待。mat是指向一个包含三个数组的数组的首地址,每次对mat
加1,将跳到第二行。
9.4 迭代器视角begin & end
上面的方法是通过mat + 3
和*row + 3
得到了尾后指针的效果,但是极容易出错。
可以使用C++11新引入的标准库函数begin
和end
。
int mat[3][3] = {{1, 2, 3},
{2, 3, 6},
{9, 3, 2}};
for (auto p = std::begin(mat); p != std::end(mat); ++p) {
for (auto q = std::begin(*p); q != std::end(*p); ++q) {
cout << *q;
}
cout << endl;
}
这样的操作就和迭代器有些相似了。
10. 总结
数组是C++中另外一种复合类型,它是一种存放多个类型相同的对象的容器,通过位置访问其中的对象。
使用数组要注意数组的声明、初始化,如何访问数组的元素,数组下标从0开始,不可以直接拷贝和赋值数组。
数组本身是一种复合类型,还可以和其他复合类型如指针、引用甚至是数组本身进行复合。
对数组的遍历可以用四种方式,最难理解的是指针的视角。