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>
.