现在,学完一维数组你可能觉得「数组嘛,就是一串连续的数据,搞定」。但现实世界里有太多数据天然就是表格状的——成绩单、棋盘、像素图片……这时候一维数组就不够用了。这篇文章我们专攻基础:二维数组是什么、怎么创建、怎么使用,以及它在内存里的真实样子。
一、先想一个问题:为什么需要二维数组?
假设你是老师,班上有 3 个学生,每个学生考了 4 门课。成绩长这样:
语文 数学 英语 体育张三 85 92 78 88李四 79 85 90 82王五 92 88 85 91
如果用一维数组存,你只能这样:
int scores[12] = {85,92,78,88, 79,85,90,82, 92,88,85,91};
问题是——拿到第 7 个数(90),它到底是哪个学生的哪门课?你得自己在脑子里算:「第 7 个 → 第二个人 → 第三门 → 李四的英语」。这不仅麻烦,代码操作也极其别扭。
二维数组就是来解决这个问题的。 它让你用「第几行、第几列」的方式直接定位数据,就像查 Excel 表格一样自然。
// 3个学生(行) × 4门课(列)int scores[3][4] = { {85, 92, 78, 88}, // 张三 {79, 85, 90, 82}, // 李四 {92, 88, 85, 91} // 王五};// scores[1][2] → 第1行第2列 → 李四的英语 → 90
好,现在你知道「为什么」了,我们正式开始。
二、二维数组到底是什么?
2.1 官方定义
二维数组:相同数据类型元素按「行」和「列」排列成的矩形集合。
- 相同数据类型 — 一个二维数组里所有元素类型一样(全是 int,或全是 double,不能混)
- 行和列
- 矩形 — 每行的列数一样多,不存在第一行 3 列、第二行 5 列的不规则形状
2.2 用图来理解
假设 int arr[2][3],逻辑结构:
列[0] 列[1] 列[2]行[0] arr[0][0] arr[0][1] arr[0][2]行[1] arr[1][0] arr[1][1] arr[1][2]
一共 2 行 × 3 列 = 6 个元素。每个元素用 数组名[行号][列号] 唯一确定。
2.3 换个角度:二维数组 = 「以一维数组为元素的数组」
这是一个非常重要的认知,慢慢说。
先回想一维数组:
a 是一个数组,里面有 3 个 int 类型的元素。
现在,把 int 这个类型换成「长度为 3 的一维 int 数组」:
理解为:arr 是一个数组,里面有 2 个元素,每个元素本身又是一个长度为 3 的一维 int 数组。
arr[0] → [ ][ ][ ] ← 这是一个 int[3] 类型的一维数组arr[1] → [ ][ ][ ] ← 这又是一个 int[3] 类型的一维数组
🧠 这个理解方式非常重要,后面学「数组指针」「动态二维数组」时你会反复用到。现在只需记住:二维数组的每一行,本质上就是一个独立的一维数组。
2.4 行号和列号都从 0 开始
C 语言所有数组下标从 0 开始:
int arr[2][3] 中:行号只能用 0、1;列号只能用 0、1、2。
三、怎么定义一个二维数组?
3.1 基本语法
3.2 举个最简例子
这行代码做了什么?
- 向内存申请
2 × 3 = 6 个 int 大小(共 6 × 4 = 24 字节)的连续空间 - 用「2 行 3 列」的逻辑视图组织这 6 个 int
但是——此时数组里都是「垃圾值」(未初始化,里面是内存残留的随机数据),跟普通变量一样,定义 ≠ 赋值。
3.3 行数和列数为什么必须是常量?
int n = 3;int arr[2][n]; // ❌ 传统C语言不允许(除非用C99变长数组,下篇讲)
编译器在编译阶段就要知道数组占多少内存。变量的值要到运行时才知道,编译时没法确定。
四、初始化——给二维数组赋初值的四种写法
这是实际写代码最频繁的操作,每种都要搞明白。
4.1 写法一:完全初始化(线性填充)
int arr[2][3] = {1, 2, 3, 4, 5, 6};
填充规则:从第 0 行第 0 列开始,从左到右、一行填满自动换到下一行,依次填入。
过程追踪:
填第1个值 1 → arr[0][0] = 1填第2个值 2 → arr[0][1] = 2填第3个值 3 → arr[0][2] = 3 ← 第0行填满,自动换行填第4个值 4 → arr[1][0] = 4填第5个值 5 → arr[1][1] = 5填第6个值 6 → arr[1][2] = 6最终:arr[0][0]=1 arr[0][1]=2 arr[0][2]=3arr[1][0]=4 arr[1][1]=5 arr[1][2]=6
💡 这种写法数据多了很难一眼看出哪个值属于哪一行。
4.2 写法二:分组初始化(大括号嵌套)⭐ 推荐
int arr[2][3] = { {1, 2, 3}, // 第0行 {4, 5, 6} // 第1行};
每对大括号 { } 代表一行,行和列一目了然,日常开发强烈推荐。
某一行没给全的情况:
int arr[2][3] = { {1, 2}, // 只给了2个值 {4} // 只给了1个值};
规则:没给到的元素,编译器自动填 0。
结果:arr[0][0]=1 arr[0][1]=2 arr[0][2]=0 ← 缺的自动补0arr[1][0]=4 arr[1][1]=0 arr[1][2]=0 ← 缺的自动补0
⚡ 记忆口诀:有就填,没有就补 0。
4.3 写法三:不完全初始化
int arr[2][3] = {1, 2, 3, 4}; // 只给了4个值
结果:
arr[0][0]=1 arr[0][1]=2 arr[0][2]=3arr[1][0]=4 arr[1][1]=0 arr[1][2]=0 ← 自动补0
C 语言规定:只要初始化列表给了至少一个值,所有未被显式赋值的元素,统统自动置为 0。
一键清零——最极端的用法:
把整个二维数组所有元素都设为 0。原理:{0} 先把 arr[0][0] 设为 0,其余元素因为「未显式赋值」自动补 0。
⚠️ {0} 只能做到全 0,不能把数组初始化成其他统一值。{1} 的结果是只有 arr[0][0] = 1,其余全是 0。
4.4 写法四:省略行号
int arr[][3] = {1, 2, 3, 4, 5, 6};
行号可以不写,编译器自己推算:6 个元素 ÷ 3 列 = 2 行。
int arr[][3] = {1, 2, 3, 4, 5, 6, 7}; // 7÷3=2余1 → 3行
但列号绝对不能省! 不告诉编译器每行几列,它怎么知道 {1,2,3,4,5,6} 该排成 2×3 还是 3×2 还是 1×6?
int arr[][] = {1,2,3,4,5,6}; // ❌ 编译报错!int arr[2][] = {1,2,3,4,5,6}; // ❌ 编译报错!
📌 黄金法则:可以省略行,绝对不能省略列。
4.5 四种初始化总结
| | |
|---|
| int a[2][3] = {1,2,3,4,5,6}; | |
| int a[2][3] = {{1,2,3},{4,5,6}}; | 日常开发首选 |
| int a[2][3] = {1,2}; | |
| int a[][3] = {1,2,3,4,5,6}; | |
🔴 碎碎念:初始化常见坑
| | | |
|---|
| int a[2][3] = {1,2,3,4,5,6,7}; | | |
| int a[2][] = {1,2,3}; | | |
| int a[2][3] = {1}; | | |
| int a[2][3]; printf("%d",a[0][0]); | | |
五、怎么访问二维数组里的元素?
5.1 用下标精确定位
访问格式:数组名[行号][列号],把它想成坐标系:
列0 列1 列2行0 [ 1 ] [ 2 ] [ 3 ]行1 [ 4 ] [ 5 ] [ 6 ]
- 想取 3?第 0 行、第 2 列 →
arr[0][2] - 想取 4?第 1 行、第 0 列 →
arr[1][0] - 想取 6?第 1 行、第 2 列 →
arr[1][2]
5.2 打印单个元素
#include<stdio.h>intmain(){ int arr[2][3] = {1, 2, 3, 4, 5, 6}; printf("%d\n", arr[0][2]); // 输出:3 printf("%d\n", arr[1][0]); // 输出:4 printf("%d\n", arr[1][2]); // 输出:6 return 0;}
arr[行号][列号] 等价于一个普通 int 变量——可以打印、赋值、参与运算:
arr[0][1] = 99; // 赋值int sum = arr[0][0] + arr[1][1]; // 参与运算
🔴 碎碎念:下标易错
六、遍历——用双重循环访问所有元素
6.1 为什么需要两层循环?
二维数组有两维(行 + 列),一层循环只能搞定一维。必须嵌套:
- 外层循环 → 控制「当前在处理第几行」
- 内层循环 → 控制「当前行里的第几列」
6.2 完整代码,逐行讲解
#include<stdio.h>intmain(){ int arr[2][3] = {1, 2, 3, 4, 5, 6}; int row = 2; // 总行数 int col = 3; // 总列数 for (int i = 0; i < row; i++) // ① 外层:第 i 行 { for (int j = 0; j < col; j++) // ② 内层:该行第 j 列 { printf("%d ", arr[i][j]); // ③ 打印当前元素 + 空格 } printf("\n"); // ④ 该行结束,换行 } return 0;}
逐行解释:
- ①
for (int i = 0; i < row; i++):i 从 0 走到 1(共 2 行),每轮处理一行 - ②
for (int j = 0; j < col; j++):j 从 0 走到 2(每行 3 列) - ③
printf("%d ", arr[i][j]):打印当前元素,%d 后面的空格让元素分开 - ④
printf("\n"):注意它在内层循环外面——一行所有列打完再换行
6.3 执行过程逐步追踪
i=0 时(处理第 0 行): j=0 → 打印 arr[0][0] = 1 j=1 → 打印 arr[0][1] = 2 j=2 → 打印 arr[0][2] = 3 内层循环结束 → 换行i=1 时(处理第 1 行): j=0 → 打印 arr[1][0] = 4 j=1 → 打印 arr[1][1] = 5 j=2 → 打印 arr[1][2] = 6 内层循环结束 → 换行i=2 → 不满足 i < row → 外层循环结束
输出:
💡 C 语言圈子里习惯 i 代表行、j 代表列。另外行优先遍历(外层行,内层列)效率最高,因为内存就是按行存储的。
七、从键盘输入数据到二维数组
7.1 先回顾 scanf
int x;scanf("%d", &x); // 从键盘读一个整数,存入变量 x
7.2 给二维数组输入数据
#include<stdio.h>intmain(){ int arr[2][3]; // 先定义,不初始化 int row = 2, col = 3; // 输入阶段 for (int i = 0; i < row; i++) for (int j = 0; j < col; j++) scanf("%d", &arr[i][j]); // 逐个读取 // 输出验证 for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) printf("%d ", arr[i][j]); printf("\n"); } return 0;}
输入过程追踪(假设你输入 10 20 30 40 50 60):
i=0, j=0: scanf 读到 10 → arr[0][0] = 10i=0, j=1: scanf 读到 20 → arr[0][1] = 20i=0, j=2: scanf 读到 30 → arr[0][2] = 30i=1, j=0: scanf 读到 40 → arr[1][0] = 40i=1, j=1: scanf 读到 50 → arr[1][1] = 50i=1, j=2: scanf 读到 60 → arr[1][2] = 60
输出:
💡 scanf 会跳过空格和换行,所以用空格分隔(10 20 30)或每行换行输入都可以。
🔴 碎碎念:输入易错点
| | | |
|---|
| scanf("%d", arr[i][j]); | scanf("%d", &arr[i][j]); | |
| scanf("%f", &arr[i][j]); | scanf("%d", &arr[i][j]); | |
| | | |
八、二维数组在内存中的真面目 ⭐⭐⭐
这是全篇最重要的部分,搞懂了它,后面指针、动态内存都不虚。
8.1 先做实验:打印每个元素的地址
#include<stdio.h>intmain(){ int arr[2][3] = {0}; for (int i = 0; i < 2; i++) for (int j = 0; j < 3; j++) printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]); return 0;}
&arr[i][j]%p- 你运行出的地址值跟我不一样,但地址之间的差值一定相同
输出示例:
&arr[0][0] = 0x6D7AFC08&arr[0][1] = 0x6D7AFC0C ← 比上一个地址大 4&arr[0][2] = 0x6D7AFC10 ← 比上一个地址大 4&arr[1][0] = 0x6D7AFC14 ← 比 arr[0][2] 大 4(跨行也是连续的!)&arr[1][1] = 0x6D7AFC18 ← 比上一个地址大 4&arr[1][2] = 0x6D7AFC1C ← 比上一个地址大 4
8.2 分析地址差
int 类型占 4 字节。看上面:
0x6D7AFC08 → 0x6D7AFC0C 差 0x0C - 0x08 = 40x6D7AFC0C → 0x6D7AFC10 差 0x10 - 0x0C = 40x6D7AFC10 → 0x6D7AFC14 差 0x14 - 0x10 = 4...
相邻元素严格差 4 字节,没有跳跃、没有空隙。
8.3 关键结论:行优先(Row-Major)连续存储
arr[0][2](第 0 行最后一个)的地址是 0x6D7AFC10,紧接着 arr[1][0](第 1 行第一个)是 0x6D7AFC14,差 4。
第 0 行和第 1 行在内存里是紧密衔接的——这就是「行优先存储」:先完整存储第 0 行的所有列,紧接着存第 1 行,再接着存第 2 行……
8.4 逻辑视图 vs 物理视图
你脑子里看到的(逻辑视图) 实际内存里存放的(物理视图)┌───────────────────────┐ ┌───┬───┬───┬───┬───┬───┐│ arr[0]: [1] [2] [3] │ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 ││ arr[1]: [4] [5] [6] │ = └───┴───┴───┴───┴───┴───┘└───────────────────────┘ 地址递增 →
🧠 核心认知:二维是给你看的,一维才是它本来的样子。二维数组在内存里就是一条长龙,只不过你用「行列」标记给它划分了段落。
8.5 万能地址公式
arr[i][j] 的地址 = 数组首地址 + (i × 总列数 + j) × sizeof(元素类型)
验证:arr[1][2] 在 int arr[2][3] 中的位置:
偏移 = (1 × 3 + 2) × 4 = (3 + 2) × 4 = 5 × 4 = 20 字节arr[1][2] 的地址 = arr[0][0]的地址 + 20 = 0x6D7AFC08 + 14(十六进制) = 0x6D7AFC1C ✅ 跟实验输出一模一样!
理解这个公式有什么用?
- 考试/面试常考题:「给定首地址,求
arr[i][j] 的地址」
🔴 碎碎念:内存相关误区
| |
|---|
| 「arr[0] 和 arr[1] 在内存里是分开的」 | |
| 不是!静态二维数组是一块连续内存,跟指针的指针完全不同 |
| 取决于类型,char 差 1,double 差 8 |
本篇(上篇)总结