Menu

C++ 数组:声明、下标访问和常见陷阱

C++ 原生数组详解:如何声明和初始化、安全地用下标访问、按长度遍历、数组退化为指针的陷阱,以及为什么 std::array 和 vector 通常更好。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

一排固定大小的值

数组是一块连续的内存,保存着固定数量的同类型值。单个 int 存储一个数字,而 int 数组则把许多个数字一个挨一个地存放,每一个都能通过整数下标访问。

声明数组时,给出元素类型、名字,以及方括号中的大小。再加上一个花括号列表来填入值:

下标从零开始:第一个元素是 scores[0],大小为 4 的数组的合法下标是 0 到 3。大小必须是编译期常量:在标准 C++ 中不能写 int n = readInput(); int a[n];(那是不可移植的扩展)。需要在运行时决定大小时,改用 vector。

初始化数组

填充数组有好几种方式,还有几个值得了解的简写:

要牢记的陷阱:当你提供的初始值少于数组大小时,剩下的元素会被值初始化(数值类型为零),而不是留下垃圾值。但完全没有初始值的数组,比如声明为局部变量的 int e[4];,里面的值是不确定的,在赋值之前读取它们属于未定义行为。

遍历数组

因为元素是连续存储的,用一个普通的下标循环就能遍历数组。为了不越界,要用数组的真实长度来控制循环,而不是写死一个数字:

sizeof(scores) 是整个数组的总字节数;除以 sizeof(scores[0])(一个元素的大小)就得到元素个数。从 C++17 开始有了更简洁的写法 std::size(scores),读起来更清楚,而且如果你不小心传给它一个指针,它会拒绝编译。如果你只需要值,还有更简单的办法:基于范围的 for 完全省去了下标计算。

越界陷阱

C++ 不会对 arr[i] 做边界检查。下标超过最后一个元素时,既不会抛出异常也不会警告,而是读写恰好位于那里的任何内存。这是最常见的数组 bug,也是典型的未定义行为:

int a[3] = {1, 2, 3};
a[3] = 99;          // OOPS - valid indices are 0..2, not 3
cout << a[5];       // garbage, crash, or corruption - undefined behavior

差一错误通常藏在循环条件里。把 i < n 写成 i <= n 会多走一步,访问到并不存在的 arr[n]:

for (int i = 0; i <= n; i++)   // BUG: when i == n, arr[i] is out of bounds
    cout << arr[i];

解决办法就是上一节讲的规矩:循环条件写 i < size,永远不要用 <=;并且从数组本身计算大小,而不是重复写一个字面量,否则添加元素后两者就会对不上。

数组退化:隐藏的指针

C++ 中最棘手的数组行为是退化(decay):当你把数组传给一个函数时,它会悄悄转换成指向第一个元素的指针。大小信息丢失了,所以函数内部的 sizeof 测量的是指针,而不是数组。

注意,作为函数参数时,int arr[] 和 int* arr 完全相同,方括号只是装饰。因为元素个数已经丢失,你必须自己把长度和数组一起传进去:

int sum(const int* arr, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) total += arr[i];
    return total;
}

这种“传一个指针和一个长度,然后祈祷它们对得上”的模式,正是促使大多数 C++ 代码转向 std::array 和 std::vector 的原因,它们自带大小,而且永远不会退化。

简单说说多维数组

把方括号嵌套起来就能构成一个网格。二维数组实际上是数组的数组,在内存中逐行排列:

用 grid[row][col] 来访问。同样的越界和退化问题依然存在,而且更严重,因为把二维数组传给函数时,除了第一维之外的每一维都必须写明(void f(int g[][3]))。除了小型的固定网格之外,vector 的 vector 要不容易出错得多。

下一篇:Vector

原生数组快速且可预测,但大小固定、没有边界保护,还会退化成指针,这些都让它们不适合日常代码。接下来我们将认识 std::vector:一种按需增长的可变大小数组,它记得自己的大小,可以直接配合 STL 算法使用,几乎提供了数组的所有功能,而让你踩坑的方式要少得多。

常见问题

C++ 中怎么声明和初始化数组?

写出元素类型、名字和方括号中的大小,然后可以选择加上花括号列表:int scores[4] = {90, 75, 100, 60};。提供了初始值时可以省略大小,写成 int scores[] = {90, 75, 100, 60};,让编译器替你数。

C++ 中怎么获取数组的长度?

对于仍在作用域内的真正数组,使用 std::size(arr)(C++17)或 sizeof(arr) / sizeof(arr[0])。一旦数组退化成了指针(例如在接收 int arr[] 参数的函数内部),这种方法就不管用了,这时 sizeof 得到的是指针的大小,而不是数组的大小。

C++ 中数组越界访问会怎样?

这是未定义行为。C++ 不会对 arr[i] 做边界检查,所以读写末尾之后的位置可能崩溃、返回垃圾值,或者悄悄破坏附近的内存。始终让下标保持在 0 到 size - 1 的范围内。

Coddy 编程语言插图

和 Coddy 一起学编程

开始