前言


二维数组的初始化看起来是入门话题,但真正写起来,选择不少:原生数组、std::array 的嵌套、std::vector 的嵌套、用一维数组模拟、以及 new 出来的动态二维数组。每种方式的内存布局、初始化语法、释放方式和传参规则都不一样,混用就会出错。


最常见的误解是「int a[3][4] = {1, 2, 3}; 会把 1、2、3 分别放进三行的第一个位置」。实际上初始化列表是按行优先(row-major)顺序依次填的:1、2、3 落在第一行的前三列,其余元素补零。另一个高频错误是把 std::vector<std::vector<int>> 当成连续内存,直接传给需要 int* 的 C 接口。


还有一个容易被忽略的点:变长数组(VLA)int a[n][m] 在 C99 里合法,但不是标准 C++。GCC 和 Clang 把它作为扩展支持,MSVC 不支持,用了就失去可移植性。


本文按「原生数组 → std::array → std::vector → 一维模拟 → 动态分配」的顺序讲,每个都给出初始化和访问的例子,最后说明传参与释放的注意事项。


一、原生二维数组


1.1 完全初始化


// 适用于 C++17
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

内层花括号可以省略,效果相同:


// 适用于 C++17
int b[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

编译器按行优先顺序依次填充,写成一行的形式可读性差,不推荐。


1.2 部分初始化


// 适用于 C++17
int c[3][4] = {{1}, {2}, {3}}; // 每行首列为 1/2/3,其余为 0
int d[3][4] = {0}; // 全部为 0
int e[3][4] = {}; // 全部为 0(C++11 起)

没有被显式初始化的元素会被值初始化为零。这一点对内置类型也成立:只要出现了初始化列表,剩余元素就是 0,而不是不确定值。相反,int f[3][4]; 这样的定义(无初始化器)在块作用域内元素是未初始化的,读取它们是未定义行为。


1.3 省略第一维


定义时第一维可以省略,编译器根据初始化列表推断:


// 适用于 C++17
int g[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}}; // 推断为 int[2][4]

第二维及以后不能省略,因为编译器需要知道每行多长才能计算内存布局。


1.4 内存布局


原生二维数组在内存中是连续的,按行优先排列:a[0][0]、a[0][1]、…、a[0][3]、a[1][0]、…。因此 &a[0][0] + i * 4 + j 就是 a[i][j] 的地址。这一点对缓存友好,也是它能直接传给 C 接口的原因。


二、std::array 与 std::vector 的嵌套


std::array 是固定大小的容器,嵌套两层就得到二维数组:


// 适用于 C++17
#include <array>

std::array<std::array<int, 4>, 3> grid{{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}};

注意初始化时用了双层花括号:外层是 std::array 的聚合初始化,内层是它的成员(内置数组)的初始化。由于聚合初始化的花括号省略规则,只写一层通常也能成立,但双层写法意图更清楚,很多代码风格指南仍要求保留。


全零初始化:


// 适用于 C++17
std::array<std::array<int, 4>, 3> zeros{}; // 所有元素为 0

std::array 的优点是有 size()、可以整体赋值、可以按值传递(虽然通常还是传引用),并且内存布局与原生数组一致,data() 返回指向首元素的指针。


2.1 std::vector 的嵌套


大小在运行期决定时,用 std::vector<std::vector<int>>:


// 适用于 C++17
#include <vector>

// 3 行 4 列,全部为 0
std::vector<std::vector<int>> grid(3, std::vector<int>(4, 0));

// 用初始化列表构造
std::vector<std::vector<int>> init{
{1, 2, 3, 4},
{5, 6, 7, 8}
};

这里有一个经典的陷阱:


// 适用于 C++17
std::vector<std::vector<int>> wrong{3, std::vector<int>(4)};

这不是「3 行 4 列」,而是「包含两个元素的 vector」:一个是 3(被隐式转换成一个只含 3 的 vector<int>),另一个是长度为 4 的 vector。花括号优先匹配 initializer_list 构造函数。正确写法用圆括号:(3, std::vector<int>(4, 0))。


vector 嵌套的内层 vector 各自独立分配,整体不是连续内存。grid[0].data() 和 grid[1].data() 之间没有保证的相邻关系,不能把 &grid[0][0] 当作 rows * cols 个连续元素使用。


三、一维数组模拟二维


需要连续内存又要运行期尺寸时,用一维 std::vector 加下标换算:


// 适用于 C++17
#include <cstddef>
#include <vector>

const std::size_t rows = 3;
const std::size_t cols = 4;

std::vector<int> grid(rows * cols, 0);

auto at = [&](std::size_t r, std::size_t c) -> int& {
return grid[r * cols + c];
};

at(0, 0) = 1;
at(2, 3) = 12;

优点:内存连续、一次分配、缓存友好、可以直接传给需要连续 int 缓冲的接口(如图像库)。缺点:下标要自己算,容易写错行列顺序;访问时没有边界检查。


C++23 提供了 std::mdspan 作为多维视图,可以在不拥有内存的前提下提供多维下标,但它的编译器支持情况差异较大,使用前应确认工具链。C++17 下可以用一个简单的包装类达到类似效果。




方式内存连续尺寸确定时机释放方式



原生 int[3][4]是编译期自动

std::array 嵌套是编译期自动

vector<vector<int>>否运行期自动

一维 vector 模拟是运行期自动

new int[3][4]是第一维可运行期delete[]



四、动态分配与传参


// 适用于 C++17
int rows = 3;
int cols = 4;

int (*grid)[4] = new int[rows][4]; // 只有第一维可以是变量

for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
grid[i][j] = 0;
}
}

delete[] grid; // 必须用 delete[]

new int[rows][4] 返回的类型是「指向 int[4] 的指针」,即 int (*)[4]。除第一维外,其余维度必须是编译期常量。释放用 delete[],用 delete 是未定义行为。


如果两维都需要运行期决定,标准做法是分配一维再换算,或者分配指针数组再逐行分配(后者不连续,释放要逐行):


// 适用于 C++17
int** rowsPtr = new int*[rows];
for (int i = 0; i < rows; ++i) {
rowsPtr[i] = new int[cols];
}
// 释放
for (int i = 0; i < rows; ++i) {
delete[] rowsPtr[i];
}
delete[] rowsPtr;

现代 C++ 更推荐用 std::vector 或一维 vector 模拟,避免手动配对 new[]/delete[]。裸 new 只在需要与 C 接口交互、或实现底层容器时才有必要。


4.1 传参


原生二维数组作为函数参数时,会退化为指向第一行的指针,第二维必须写出来:


// 适用于 C++17
void fill(int arr[][4], int rows) { // 等价于 int (*arr)[4]
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
arr[i][j] = i * 4 + j;
}
}
}

std::array 和 std::vector 用引用传参,避免拷贝:


// 适用于 C++17
void use(const std::array<std::array<int, 4>, 3>& grid);
void use(const std::vector<std::vector<int>>& grid);

完整示例


// 适用于 C++17
#include <array>
#include <iostream>
#include <vector>

int main() {
int native[2][3] = {{1, 2, 3}, {4, 5, 6}};

std::array<std::array<int, 3>, 2> arr{{
{1, 2, 3},
{4, 5, 6}
}};

std::vector<std::vector<int>> vec(2, std::vector<int>(3, 0));
vec[1][2] = 6;

std::vector<int> flat(2 * 3, 0);
flat[1 * 3 + 2] = 6;

int partial[2][3] = {{7}, {8}};

std::cout << native[1][1] << ' ' << arr[0][2] << ' '
<< vec[1][2] << ' ' << flat[5] << '\n';
std::cout << partial[0][1] << ' ' << partial[1][0] << '\n';

return 0;
}

按标准语义,第一行输出 5 3 6 6;partial 的第一行是 {7, 0, 0},第二行是 {8, 0, 0},所以第二行输出 0 8。


常见坑点



  1. ❌ 使用变长数组:


``cpp int n = 10; int a[n][n]; // 不是标准 C++ ` ✅ 用 std::vector<std::vector<int>> 或一维 std::vector`。



  1. ❌ 以为 {1, 2, 3} 是逐行填充:


``cpp int a[2][3] = {1, 2, 3}; // 全在第一行,第二行全 0 ` ✅ 明确写内层花括号:{{1, 2, 3}, {4, 5, 6}}`。



  1. ❌ new int[3][4] 用 delete 释放:


``cpp delete grid; // UB ` ✅ delete[] grid;`。



  1. ❌ 用花括号构造 vector<vector<int>> 的行列:


``cpp std::vector<std::vector<int>> g{3, std::vector<int>(4)}; // 2 个元素 ` ✅ 用圆括号:g(3, std::vector<int>(4, 0))`。



  1. ❌ 把 vector<vector<int>> 当连续内存传给 C 接口:


``cpp c_function(&grid[0][0]); // 只有第一行是连续的 ` ✅ 改用一维 std::vector<int>,传 grid.data()`。



  1. ❌ 函数参数写 void f(int a[][]):


``cpp void f(int a[][]); // 编译错误 ` ✅ 写出第二维:void f(int a[][4], int rows)`。



  1. ❌ 用 memset 初始化含非平凡类型的二维 vector:


``cpp std::memset(&grid[0][0], 0, sizeof(int) * n); // 对 vector 无效且越界 ` ✅ 用构造函数或 std::fill`。



  1. ❌ 读取未初始化的局部数组:


``cpp int a[3][3]; int x = a[0][0]; // 未定义行为 ` ✅ 定义时初始化:int a[3][3] = {};`。


总结




场景推荐方式



尺寸编译期固定、追求性能原生数组或 std::array 嵌套

尺寸运行期决定、行独立std::vector<std::vector<int>>

尺寸运行期决定、要连续内存一维 std::vector + 下标换算

与 C 接口交互一维连续缓冲或 new int[n][4]

需要避免手动释放一律用容器



二维数组的选择本质是三个维度的权衡:尺寸何时确定、内存是否要求连续、由谁负责释放。把这三点的答案定下来,写法自然就确定了。初始化时记住「按行优先、缺省补零、花括号匹配 initializer_list」三条,大部分坑都能提前避开。




Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐