数据结构:二维数组(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<vector<int>>)


方式 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** Avector<vector<int>>

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


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

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

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

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

  3. 指针是变量的地址

  4. 一个数组可以存放指针

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

  6. 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]
相关推荐
刘海东刘海东13 分钟前
结构型智能科技的关键可行性——信息型智能向结构型智能的转变(修改提纲)
人工智能·算法·机器学习
pumpkin8451440 分钟前
Rust 调用 C 函数的 FFI
c语言·算法·rust
挺菜的1 小时前
【算法刷题记录(简单题)003】统计大写字母个数(java代码实现)
java·数据结构·算法
mit6.8241 小时前
7.6 优先队列| dijkstra | hash | rust
算法
2401_858286111 小时前
125.【C语言】数据结构之归并排序递归解法
c语言·开发语言·数据结构·算法·排序算法·归并排序
guygg882 小时前
基于matlab的FIR滤波器
开发语言·算法·matlab
双叶8362 小时前
(C++)学生管理系统(正式版)(map数组的应用)(string应用)(引用)(文件储存的应用)(C++教学)(C++项目)
c语言·开发语言·数据结构·c++
ysh98883 小时前
PP-OCR:一款实用的超轻量级OCR系统
算法
遇雪长安3 小时前
差分定位技术:原理、分类与应用场景
算法·分类·数据挖掘·rtk·差分定位
数通Dinner3 小时前
RSTP 拓扑收敛机制
网络·网络协议·tcp/ip·算法·信息与通信