前言二维数组的初始化看起来是入门话题但真正写起来选择不少原生数组、std::array的嵌套、std::vector的嵌套、用一维数组模拟、以及new出来的动态二维数组。每种方式的内存布局、初始化语法、释放方式和传参规则都不一样混用就会出错。最常见的误解是「int a[3][4] {1, 2, 3};会把 1、2、3 分别放进三行的第一个位置」。实际上初始化列表是按行优先row-major顺序依次填的1、2、3 落在第一行的前三列其余元素补零。另一个高频错误是把std::vectorstd::vectorint当成连续内存直接传给需要int*的 C 接口。还有一个容易被忽略的点变长数组VLAint a[n][m]在 C99 里合法但不是标准 C。GCC 和 Clang 把它作为扩展支持MSVC 不支持用了就失去可移植性。本文按「原生数组 →std::array→std::vector→ 一维模拟 → 动态分配」的顺序讲每个都给出初始化和访问的例子最后说明传参与释放的注意事项。一、原生二维数组1.1 完全初始化// 适用于 C17int a[3][4] {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};内层花括号可以省略效果相同// 适用于 C17int b[3][4] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};编译器按行优先顺序依次填充写成一行的形式可读性差不推荐。1.2 部分初始化// 适用于 C17int c[3][4] {{1}, {2}, {3}}; // 每行首列为 1/2/3其余为 0int d[3][4] {0}; // 全部为 0int e[3][4] {}; // 全部为 0C11 起没有被显式初始化的元素会被值初始化为零。这一点对内置类型也成立只要出现了初始化列表剩余元素就是 0而不是不确定值。相反int f[3][4];这样的定义无初始化器在块作用域内元素是未初始化的读取它们是未定义行为。1.3 省略第一维定义时第一维可以省略编译器根据初始化列表推断// 适用于 C17int 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是固定大小的容器嵌套两层就得到二维数组// 适用于 C17#include arraystd::arraystd::arrayint, 4, 3 grid{{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}}};注意初始化时用了双层花括号外层是std::array的聚合初始化内层是它的成员内置数组的初始化。由于聚合初始化的花括号省略规则只写一层通常也能成立但双层写法意图更清楚很多代码风格指南仍要求保留。全零初始化// 适用于 C17std::arraystd::arrayint, 4, 3 zeros{}; // 所有元素为 0std::array的优点是有size()、可以整体赋值、可以按值传递虽然通常还是传引用并且内存布局与原生数组一致data()返回指向首元素的指针。2.1 std::vector 的嵌套大小在运行期决定时用std::vectorstd::vectorint// 适用于 C17#include vector// 3 行 4 列全部为 0std::vectorstd::vectorint grid(3, std::vectorint(4, 0));// 用初始化列表构造std::vectorstd::vectorint init{{1, 2, 3, 4},{5, 6, 7, 8}};这里有一个经典的陷阱// 适用于 C17std::vectorstd::vectorint wrong{3, std::vectorint(4)};这不是「3 行 4 列」而是「包含两个元素的 vector」一个是3被隐式转换成一个只含 3 的vectorint另一个是长度为 4 的 vector。花括号优先匹配initializer_list构造函数。正确写法用圆括号(3, std::vectorint(4, 0))。vector嵌套的内层 vector 各自独立分配整体不是连续内存。grid[0].data()和grid[1].data()之间没有保证的相邻关系不能把grid[0][0]当作rows * cols个连续元素使用。三、一维数组模拟二维需要连续内存又要运行期尺寸时用一维std::vector加下标换算// 适用于 C17#include cstddef#include vectorconst std::size_t rows 3;const std::size_t cols 4;std::vectorint 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缓冲的接口如图像库。缺点下标要自己算容易写错行列顺序访问时没有边界检查。C23 提供了std::mdspan作为多维视图可以在不拥有内存的前提下提供多维下标但它的编译器支持情况差异较大使用前应确认工具链。C17 下可以用一个简单的包装类达到类似效果。方式内存连续尺寸确定时机释放方式原生int[3][4]是编译期自动std::array嵌套是编译期自动vectorvectorint否运行期自动一维vector模拟是运行期自动new int[3][4]是第一维可运行期delete[]四、动态分配与传参// 适用于 C17int 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是未定义行为。如果两维都需要运行期决定标准做法是分配一维再换算或者分配指针数组再逐行分配后者不连续释放要逐行// 适用于 C17int** 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 传参原生二维数组作为函数参数时会退化为指向第一行的指针第二维必须写出来// 适用于 C17void 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用引用传参避免拷贝// 适用于 C17void use(const std::arraystd::arrayint, 4, 3 grid);void use(const std::vectorstd::vectorint grid);完整示例// 适用于 C17#include array#include iostream#include vectorint main() {int native[2][3] {{1, 2, 3}, {4, 5, 6}};std::arraystd::arrayint, 3, 2 arr{{{1, 2, 3},{4, 5, 6}}};std::vectorstd::vectorint vec(2, std::vectorint(3, 0));vec[1][2] 6;std::vectorint 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 6partial的第一行是{7, 0, 0}第二行是{8, 0, 0}所以第二行输出0 8。常见坑点❌ 使用变长数组cpp int n 10; int a[n][n]; // 不是标准 C✅ 用std::vectorstd::vectorint或一维std::vector。❌ 以为{1, 2, 3}是逐行填充cpp int a[2][3] {1, 2, 3}; // 全在第一行第二行全 0✅ 明确写内层花括号{{1, 2, 3}, {4, 5, 6}}。❌new int[3][4]用delete释放cpp delete grid; // UB✅delete[] grid;。❌ 用花括号构造vectorvectorint的行列cpp std::vectorstd::vectorint g{3, std::vectorint(4)}; // 2 个元素✅ 用圆括号g(3, std::vectorint(4, 0))。❌ 把vectorvectorint当连续内存传给 C 接口cpp c_function(grid[0][0]); // 只有第一行是连续的✅ 改用一维std::vectorint传grid.data()。❌ 函数参数写void f(int a[][])cpp void f(int a[][]); // 编译错误✅ 写出第二维void f(int a[][4], int rows)。❌ 用memset初始化含非平凡类型的二维vectorcpp std::memset(grid[0][0], 0, sizeof(int) * n); // 对 vector 无效且越界✅ 用构造函数或std::fill。❌ 读取未初始化的局部数组cpp int a[3][3]; int x a[0][0]; // 未定义行为✅ 定义时初始化int a[3][3] {};。总结场景推荐方式尺寸编译期固定、追求性能原生数组或std::array嵌套尺寸运行期决定、行独立std::vectorstd::vectorint尺寸运行期决定、要连续内存一维std::vector 下标换算与 C 接口交互一维连续缓冲或new int[n][4]需要避免手动释放一律用容器二维数组的选择本质是三个维度的权衡尺寸何时确定、内存是否要求连续、由谁负责释放。把这三点的答案定下来写法自然就确定了。初始化时记住「按行优先、缺省补零、花括号匹配 initializer_list」三条大部分坑都能提前避开。