数据结构:二维数组(2D Arrays)

数据结构:二维数组(2D Arrays)

目录

什么是二维数组?

二维数组的声明方式

[方式 1:静态二维数组](#方式 1:静态二维数组)

[方式 2:数组指针数组(数组中存放的是指针)](#方式 2:数组指针数组(数组中存放的是指针))

[方式 3:双指针 + 二级堆分配](#方式 3:双指针 + 二级堆分配)

[💡 补充建议](#💡 补充建议)

[如何用"第一性原理"去推导出 C++ 中二维数组的三种声明方式?](#如何用“第一性原理”去推导出 C++ 中二维数组的三种声明方式?)

[第一阶段:内存连续,列固定,行固定 → 推导出方式①](#第一阶段:内存连续,列固定,行固定 → 推导出方式①)

[第二阶段:每行独立、列可能不同(不规则矩阵) → 推导出方式②](#第二阶段:每行独立、列可能不同(不规则矩阵) → 推导出方式②)

[第三阶段:行数和列数都是运行时才知道的 → 推导出方式③](#第三阶段:行数和列数都是运行时才知道的 → 推导出方式③)

什么是二维数组?

二维数组本质上是"数组的数组",你可以将它看作一个矩阵,每个元素有两个索引:

cpp

复制代码

int A[3][4];

表示一个包含 3 行 4 列 的整数矩阵。

访问方式是 A[row][col],例如 A[1][2] 表示第 2 行第 3 列。

二维数组的声明方式

我们来逐一讲解,并图示结构 + 内存布局图解:

方式 1:静态二维数组

cpp

复制代码

int A[3][4] = {

{1, 2, 3, 4},

{5, 6, 7, 8},

{9, 10,11,12}

};

结构理解:

这是一个连续内存块,编译器分配的实际大小为 3 * 4 * sizeof(int),总共 12 个整型元素。

图示结构:

cpp

复制代码

A: → 3 行 × 4 列的矩阵(行优先排列)

[1, 2, 3, 4]

[5, 6, 7, 8]

[9,10,11,12]

💾 内存模型(连续存储):

cpp

复制代码

+----+----+----+----+----+----+----+----+----+----+----+----+ ← 低地址

| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

+----+----+----+----+----+----+----+----+----+----+----+----+ ← 高地址

按行顺序(row-major)存储:第一行 → 第二行 → 第三行

特点:

属性

说明

内存连续

高效访问

易传给函数

是(需行数、列数信息)

可初始化所有值

灵活性(行数/列数变动)

否 不可动态变长

简单高效

编译器在编译时就知道每个元素地址的偏移

适用于尺寸固定的表格/矩阵处理

优势体现:性能最佳(连续内存 + 快速索引)

📌 示例:加速矩阵乘法(靠缓存命中率)

cpp

复制代码

int A[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};

int B[4][2] = {{1,2}, {1,2}, {1,2}, {1,2}};

int C[3][2] = {0};

for (int i = 0; i < 3; ++i)

for (int j = 0; j < 2; ++j)

for (int k = 0; k < 4; ++k)

C[i][j] += A[i][k] * B[k][j];

优势体现点:

编译器知道 A[i][k] 和 B[k][j] 在内存中的确切偏移

数据连续,CPU 缓存命中率高

运行速度远优于动态结构(如 vector>)

方式 2:数组指针数组(数组中存放的是指针)

cpp

复制代码

int* A[3]; // 数组中存放3个 int* 指针

A[0] = new int[4];

A[1] = new int[4];

A[2] = new int[4];

结构理解:

A 是一个大小为 3 的指针数组:A[0], A[1], A[2]

每个 A[i] 是一个指向 长度为 4 的数组 的指针

图示结构:

cpp

复制代码

A: → [指针] [指针] [指针]

↓ ↓ ↓

[1,2,3,4] [5,6,7,8] [9,10,11,12]

💾 内存模型(不连续):

cpp

复制代码

栈:

+-----+-----+-----+ ← A[0], A[1], A[2] 是在栈区的指针变量

| ptr | ptr | ptr |

+-----+-----+-----+

↓ ↓ ↓

堆:

[1][2][3][4] // A[0] 指向此处

[5][6][7][8] // A[1]

[9][10][11][12] // A[2]

特点:

属性

说明

内存连续?

否 每一行在堆中独立分配

指针存储在哪里?

是 指针数组本身在栈

灵活性?

是 可以变行数或列数

缺点

否 不利于整体拷贝、效率较低、复杂性高

比方式①更灵活

每一行都可以单独分配、单独修改、单独释放

行长度可以不同("不规则二维数组")

优势体现:行长不一致的表格处理

📌 示例:一个表格,有 3 行,每行元素个数不同

cpp

复制代码

int* A[3];

A[0] = new int[2]{1,2}; // 第一行只有2个元素

A[1] = new int[3]{3,4,5}; // 第二行3个

A[2] = new int[4]{6,7,8,9}; // 第三行4个

for (int i = 0; i < 2; ++i) cout << A[0][i] << " ";

for (int i = 0; i < 3; ++i) cout << A[1][i] << " ";

for (int i = 0; i < 4; ++i) cout << A[2][i] << " ";

delete[] A[0]; delete[] A[1]; delete[] A[2];

优势体现点:

不规则矩阵结构(每行不一样)

适合表示数据表格(例如 JSON 表、数据库行)

方式 3:双指针 + 二级堆分配

cpp

复制代码

int** A;

A = new int*[3]; // 分配3个指针

A[0] = new int[4];

A[1] = new int[4];

A[2] = new int[4];

结构理解:

A 是一个指向指针的指针:int**

它指向一个动态分配的指针数组

每个指针再指向各自的行数组

图示结构:

cpp

复制代码

堆1(A):

+-----+-----+-----+

| ptr | ptr | ptr | ← A[0], A[1], A[2]

+-----+-----+-----+

↓ ↓ ↓

堆2:

[1,2,3,4] [5,6,7,8] [9,10,11,12]

💾 内存模型(全堆 + 不连续):

cpp

复制代码

栈:

+----------------+ ← A(int** 类型,指向堆中指针数组)

| 指向堆的指针地址 |

+----------------+

堆(第一次 new):

+-----+-----+-----+

| ptr | ptr | ptr | ← 存放 A[0], A[1], A[2]

+-----+-----+-----+

堆(3次 new):

[1,2,3,4] ← A[0]

[5,6,7,8] ← A[1]

[9,10,11,12] ← A[2]

特点:

属性

说明

全部在堆中?

(适合大型数据)

是否连续?

不连续,多次分配

灵活性?

最高,可以动态控制行/列数

管理复杂度?

需手动 delete 两层,容易泄漏

最具通用性

完全动态二维数组

行数、列数在运行时可变,甚至可以动态增删行列

优势体现:完全运行时控制二维矩阵大小

📌 示例:用户输入决定行列数

cpp

复制代码

int rows, cols;

cin >> rows >> cols;

int** A = new int*[rows];

for (int i = 0; i < rows; ++i)

A[i] = new int[cols]; // 每行动态开辟

// 初始化矩阵

for (int i = 0; i < rows; ++i)

for (int j = 0; j < cols; ++j)

A[i][j] = i * cols + j;

// 打印矩阵

for (int i = 0; i < rows; ++i) {

for (int j = 0; j < cols; ++j)

cout << A[i][j] << " ";

cout << endl;

}

// 释放内存

for (int i = 0; i < rows; ++i) delete[] A[i];

delete[] A;

优势体现点:

非常适合处理用户输入、数据文件、图结构(邻接表)

最接近"二维 vector"的行为

💡 补充建议

如果你只需要 固定结构,首选 int A[3][4]

如果你需要 运行时决定行列,用 int** A 或 vector>

如果你想用更现代 C++ 写法:推荐使用 std::vector 来替代动态数组

如何用"第一性原理"去推导出 C++ 中二维数组的三种声明方式?

基础事实(不依赖高层语法):

内存是线性地址空间(一条一维线)

数组是连续的同类型内存块

指针是变量的地址

一个数组可以存放指针

new / malloc 可以在运行时分配任意大小的内存

C++ 编译器可以通过 arr[i][j] 翻译成偏移运算:*( *(arr + i) + j )

🔍 我的问题目标:我想建一个二维表格

输入需求:

我希望能访问 A[i][j]

这个结构要能容纳多行多列的数据

第一阶段:内存连续,列固定,行固定 → 推导出方式①

设计需求:

所有元素个数是 M × N

所有行列大小都是固定的

适合做 数值型矩阵处理、图像、表格

🤔 思考过程:

❓ 我能不能一次申请一个大数组,把所有数据按行拼在一起?

当然可以,我做个拍扁的线性数组:

cpp

复制代码

int data[M * N];

然后我写一个访问函数:

cpp

复制代码

int get(int i, int j) {

return data[i * N + j];

}

👉 很方便,但语法不直观。我希望写 A[i][j] 而不是 data[i * N + j]

于是我想:

cpp

复制代码

int A[M][N];

这不就是把 M*N 连续空间组织成二维格式了吗?

A[i][j] 被编译器翻译为:

cpp

复制代码

*(A + i*N + j) = *( *(A + i) + j )

这就推导出了:

✅方式①:int A[3][4]

第二阶段:每行独立、列可能不同(不规则矩阵) → 推导出方式②

新设计需求:

每行的长度不同

不想为每一行都分配一样多空间(内存浪费)

比如:

cpp

复制代码

Row 0: 3 elements

Row 1: 5 elements

Row 2: 2 elements

思考过程:

❓ 我能否为每一行单独分配一个一维数组?

当然可以,我可以写:

cpp

复制代码

int* row0 = new int[3];

int* row1 = new int[5];

int* row2 = new int[2];

那我怎么管理这些行?

💡 我想到了:把这些指针放进一个数组中!

cpp

复制代码

int* A[3]; // A[0], A[1], A[2] 分别指向三行

A[0] = row0;

A[1] = row1;

A[2] = row2;

现在我就可以访问 A[i][j] 了:

A[i] 是一行指针

A[i][j] 是访问这一行中的元素

这就推导出了:

✅方式②:int* A[3];

第三阶段:行数和列数都是运行时才知道的 → 推导出方式③

新设计需求:

用户在运行时告诉我有多少行和列

比如:

cpp

复制代码

cin >> rows >> cols;

❓ 我该怎么在运行时分配二维表格呢?

我不能用 int A[rows][cols],因为在 C++ 中数组大小必须是编译时常量(除 VLA 编译器扩展)

🤔 思考过程:

第一步:

我要创建一个 行指针数组:

cpp

复制代码

int** A = new int*[rows];

A 就是一个"二维数组的外壳":A[i] 将是每一行的指针

第二步:

为每一行动态创建列空间:

cpp

复制代码

for (int i = 0; i < rows; ++i)

A[i] = new int[cols];

这样就构成了一个"指针的指针":二维结构。

得到:

✅方式③:int** A; A = new int*[rows]; A[i] = new int[cols];

总结:需求驱动 + 构造能力 → 推导出三种方式

场景/需求

你能用的构造

推导出的结构

实际语法

数据量固定、行列固定

数组

连续二维数组

int A[M][N]

行数固定、列数可变

数组 + 指针

指针数组 + 行数组

int* A[M];

行列都可变

双层指针 + 堆分配

二级动态数组

int** A;

cpp

复制代码

目标:二维数据结构

选择1:一维内存线? → 折叠二维到一维 → A[i][j] ≈ A[i*N + j] → ✅ int A[M][N]

选择2:能不能每行分开存? → 每行是 int*,用指针数组存行 → ✅ int* A[M]

选择3:连行数都未知? → 指针数组也要动态分配 → ✅ int** A; A = new int*[M]

💎 相关推荐

【求助】化学诱变剂的处理
365app安卓客户端下载

【求助】化学诱变剂的处理

📅 08-22 👁️ 2922
请教一下,GM查证一般需要多久,谢谢
英国365bet日博

请教一下,GM查证一般需要多久,谢谢

📅 11-04 👁️ 444
捕鱼大作战vip价格表 怎么充值划算
365app安卓客户端下载

捕鱼大作战vip价格表 怎么充值划算

📅 10-19 👁️ 3446