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) 等价, 是按照被指向类型的大小发生偏移, 然后解引用,

../../../_images/begin_size.gif

指针的偏移#

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}
../../../_images/begin_size.gif

如何确定传入的数组长度为 0 呢?

1bool is_empty(int const* array, int size) {
2  return size == 0;
3}
../../../_images/begin_size_empty.png

更好的解决方案: (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}
../../../_images/begin_end.gif

你可以简单转换得到数组的长度.

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}
../../../_images/begin_end_empty.png

特殊方案: 在数组末尾用一个特殊值表示结束#

比如我们以 -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>.

相关解答#