C 风格数组: T array[size] (C-style array)#
有时候我们不止需要一个对象, 而需要一组同类型的对象.
由此有了 C 风格数组 T array[size], 它连续存储一组同类型的对象 (称为数组中的元素), 并允许以 array[index] 方式访问对应位置的元素.
1int array[5] = {0, 1, 2, 3, 4};
2// ↓ 返回类型的存储大小
3sizeof(array) == sizeof(int) * 5; // 连续存储了 5 个元素, 自然大小是 sizeof(int) * 5 了
4sizeof(array[0]) == sizeof(int); // array[0] 访问第一个位置的元素
5
6int const size = sizeof(array) / sizeof(array[0]); // 数组总大小 / 元素大小 = 5
7for (int i = 0; i < size; ++i) { // 输出数组内元素
8 std::cout << array[i] << ' ';
9}
10std::cout << '\n';
需要注意的是, C++ 标准下 C 风格数组的长度必须是一个常量.
1int size;
2std::cin >> size;
3int array[size]; // 错误: size 在编译时未知, 运行时才输入了值
你可以省略长度而在声明时初始化, 编译器会根据初始化列表推测数组长度.
1int array[] = {1, 2, 3, 4, 5};
2int const size = sizeof(array) / sizeof(array[0]); // 长度为 5
记录数组的长度#
在上面的代码中, 我们也可以在需要用到数组长度的位置, 直接填入它的长度 5, 但我们凭什么说代码里遇到的 5 都是数组的长度呢? 想象一下, 你用 int array[5] 编写了一千行代码, 后来需要将数组的长度 5 改为 10, 你需要仔细地查看一千行代码, 一个个改动.
更好的方法是, 使用一个变量存储数组的长度, 以后只需要改变这个变量的值.
1int const size = 5; // 这和 const int size 是等价的
2int array[size] = {0, 1, 2, 3, 4};
3
4for (int i = 0; i < size; ++i) {
5 std::cout << array[i] << ' ';
6}
7std::cout << '\n';
相关核心准则
C 风格数组很容易隐式类型转换为指向首元素的指针#
C 风格数组 很容易很容易很容易 隐式类型转换为指向首元素的指针,
比如参与运算时,
1int array[5] = {0, 1, 2, 3, 4};
2sizeof(array) == sizeof(int) * 5; // 数组本身的大小
3sizeof(+array) == sizeof(int*); // 算术运算时转换为首元素的指针
4sizeof(array + 0) == sizeof(int*); // 算术运算时转换为首元素的指针
5sizeof(array + 1) == sizeof(int*); // 算术运算时转换为首元素的指针, +1 后为第二个元素的指针
比如发生拷贝时,
1int array[5] = {0, 1, 2, 3, 4};
2// ↓ auto: 我不知道它的类型是啥, 但我是通过拷贝 array 得到的 value, 编译器你自己分析类型是啥
3auto value = array; // 拷贝时转换为首元素的指针
4sizeof(value) == sizeof(int*);
5
6int* pointer = array; // 与上面等价
7sizeof(pointer) == sizeof(int*);
如果是在一个函数中, 这样做没有什么影响, 毕竟指针也可以以下标访问内容,
1int array[5] = {0, 1, 2, 3, 4};
2int* pointer = array;
3
4for (int i = 0; i < 5; ++i) {
5 pointer[i] = 0;
6}
你可以认为, pointer[i] 与 *(pointer + i) 等价, 是按照被指向类型的大小发生偏移, 然后解引用,
指针的偏移#
1int array[5] = {0, 1, 2, 3, 4};
2int* pointer = array;
3
4for (int i = 0; i < 5; ++i) {
5 *(pointer + i) = 0;
6}
数组的传参#
问题#
我们需要向函数传递数组, 而有时候函数并不应该改变数组的内容, 所以我们应该拷贝数组, 对吧?
1void function(int array[5]) {
2 array[0] = 5;
3}
4
5int main() {
6 int array[5] = {0, 1, 2, 3, 4};
7
8 function(array);
9
10 // 注意: 输出 5 1 2 3 4
11 for (int i = 0; i < 5; ++i) {
12 std::cout << i << ' ';
13 }
14 std::cout << '\n';
15}
这怎么会影响到 main 函数里的 array? 我们在 函数 (function) 中得知, 传参可以当作声明变量来理解, 回顾一下, 我们刚刚用数组拷贝创建新变量时发生了什么?
1int array[5] = {0, 1, 2, 3, 4};
2auto value = array; // 这个 value 是指向数组首元素的指针!
所以, 我们看起来是拷贝了数组, 实际上只是获取了指向数组首元素的指针.
这样拷贝后两个变量实际是指向同一个对象的语义, 称为引用语义; 与之相对地, 像 int 那样拷贝后得到确确实实的新对象, 与原来的对象完全独立, 称为值语义. 尽量避免引用语义.
提示
你也可以使用断点调试自己验证一下函数内 array 的实际类型. 断点调试非常有用, 请学习 断点调试的使用 并完成习题.
甚至, 以下函数实际是同样的函数:
1// 实际都是 void function(int* array)
2void function(int* array);
3void function(int array[]);
4void function(int array[5]);
好吧, 为了避免对不知道这条规则的人的欺骗, 让我们将函数直接写成 void function(int* array), 由此可推测, 如果我们确实不想改动数组, 我们可以用 void function(int const* array).
但现在的问题是, 仅仅传入指向数组首元素的指针就够了吗?
1void function(int* array) {
2 for (int i = 0; i < 5; ++i) { // 长度为 5
3 array[i] = 0;
4 }
5}
6
7int main() {
8 int array[3] = {1, 2, 3}; // 长度为 3
9 function(array);
10}
我们在函数内不知道数组的长度是多少. 不, 在函数内使用 sizeof(array) / sizeof(int) 并没有用. 前面说过, 由于拷贝传参时发生隐式类型转换, 我们只是传入了指向首元素的指针, 那么 sizeof(array) 只能得到指针 int* 的大小, 而不是数组的总大小——由于隐式类型转换, 长度信息已经丢失了.
所以我们如果这样传参, 则在函数内没有长度信息. 更具体地, 我们不知道什么时候结束循环!
解决方案: (int* array, int size)#
不知道数组的长度是多少, 那么我们就传入数组的长度.
1void print(int const* array, int size) {
2 for (int i = 0; i < size; ++i) {
3 std::cout << array[i] << ' ';
4 }
5 std::cout << '\n';
6}
如何确定传入的数组长度为 0 呢?
1bool is_empty(int const* array, int size) {
2 return size == 0;
3}
更好的解决方案: (int* begin, int* end)#
不知道什么时候结束, 那么我们就告知什么时候结束, 把指向结束位置的指针——也就是指向末尾元素之后一个位置 (逾尾位置) 的指针——传给函数.
1void print(int const* begin, int const* end) {
2 for (; begin != end; ++begin) {
3 std::cout << *begin << ' ';
4 }
5 std::cout << '\n';
6}
你可以简单转换得到数组的长度.
1int size(int const* begin, int const* end) {
2 return end - begin;
3}
如何确定传入的数组长度为 0 呢? 长度为 0 意味着指向开始位置的指针也指向逾尾位置.
1bool is_empty(int const* begin, int const* end) {
2 return begin == end;
3}
特殊方案: 在数组末尾用一个特殊值表示结束#
比如我们以 -1 作为终止值.
1void print(int const* array) {
2 for (; *array != -1; ++array) {
3 std::cout << *array << ' ';
4 }
5 std::cout << '\n';
6}
7
8int main() {
9 int array[6] = {1, 2, 3, 4, 5, -1};
10 print(array);
11}
你可以简单遍历得到数组的有效长度.
1void size(int const* array) {
2 int count = 0;
3 for (; *array != -1; ++array) {
4 ++count;
5 }
6 return count;
7}
8
9int main() {
10 int array[6] = {1, 2, 3, 4, 5, -1};
11 int size = size(array); // 有效长度为 5
12}
提示
有没有感觉眼熟? 字符串就是这么做的!
std::strlen(string) 是怎么获取字符串长度的? 从左到右一直数到 '\0'.
1int my_strlen(char const* string) {
2 int count = 0;
3 for (; *string != '\0'; ++string) {
4 ++count;
5 }
6 return count;
7}
警告
需要注意的是, 这种方式因为需要定义终止值而并不能泛用; 别人要是用你的函数必须也采用同样的终止值.
想想你的 char array[3] = {'a', 'b', 'c'} 为什么输出出奇怪的内容! 因为你没有加上终止字符 '\0'.
思维启发: 我一定要传入整个数组吗?#
请思考以下代码:
1void print(int const* begin, int const* end);
2
3int main() {
4 int array[5] = {0, 1, 2, 3, 4};
5 print(array + 1, array + 3); // 输出 1 2
6 print(array + 2, array + 5); // 输出 2 3 4
7}
最佳实践#
用另外的变量 (如
int const size) 记录数组的长度.传参时使用
(int* array, int size)或(int* begin, int* end).
更好地, 去学习使用 std::array<T, size> 和 std::vector<T>.