引言
二维数组是 C 语言中重要的数据结构,而它与指针的结合更是让许多学习者感到困惑。本文将通过内存布局图与大量代码示例,彻底讲清二维数组的本质、数组指针与指针数组的区别,以及各种指针运算的规则。
一、二维数组的内存布局
二维数组在内存中是连续存储的,按行优先排列。理解这一点是掌握二维数组与指针的关键。
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
这段代码在内存中的布局如下:
地址: 0x00 0x04 0x08 0x0C 0x10 0x14 0x18 0x1C 0x20 0x24 0x28 0x2C
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
数据: │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
行0 ──────────►│ 行1 ──────────►│ 行2 ──────────►│
注意:虽然逻辑上是"3行4列",但物理内存只有连续的 12 个 int 元素,没有任何"行与行之间的分隔符"。
二、二维数组名到底是什么?
在表达式中,二维数组名会退化为一维数组的指针,即"指向第一行"的指针。
int arr[3][4];
// arr 的类型是 int (*)[4],即"指向包含4个int的数组"的指针
printf("%p\n", (void*)arr); // &arr[0]
printf("%p\n", (void*)arr[0]); // &arr[0][0],值相同但类型不同!
printf("%zu\n", sizeof(arr)); // 3*4*4 = 48,整个数组大小
printf("%zu\n", sizeof(arr[0])) ; // 4*4 = 16,第一行的大小
sizeof 与取地址操作会阻止数组名退化,所以这两个表达式可以观察到数组的真正类型。
三、数组指针 vs 指针数组
这是最容易混淆的概念,我们通过代码彻底区分:
3.1 数组指针
是指向数组的指针,本质上是指针,只是指向的对象是数组。
// 定义一个指针,指向包含4个int的数组
int (*p)[4] = arr; // p 是"数组指针",p+1 跳过的单位是 4*4=16 字节
printf("%d\n", (*p)[0]); // 1,等价于 arr[0][0]
printf("%d\n", (*p)[3]); // 4,等价于 arr[0][3]
printf("%d\n", p[1][0]); // 5,等价于 arr[1][0]
p++; // p 现在指向 arr[1]
printf("%d\n", p[0][0]); // 5,等价于 *(*p)
3.2 指针数组
是数组,数组的每个元素都是指针。
// 定义一个数组,包含4个int*指针
int *ap[4]; // ap 是"指针数组",4个元素都是 int*
int a=1, b=2, c=3, d=4;
ap[0] = &a;
ap[1] = &b;
ap[2] = &c;
ap[3] = &d;
printf("%d %d %d %d\n", *ap[0], *ap[1], *ap[2], *ap[3]);
// 输出: 1 2 3 4
区分技巧:
-
-
-
int (*p)[4]:p 先与 * 结合,是指针-
int *ap[4]:ap 先与 [] 结合,是数组四、行指针与列指针
在二维数组中,有两种"行走"方式:
int arr[3][4] = {0};
// arr 本身:int (*)[4],跳一行(4个元素)
int (*row_ptr)[4] = arr;
// arr[0]:int*,跳一个元素
int *col_ptr = arr[0];
printf("row_ptr + 1 = %p\n", (void*)(row_ptr + 1));
printf("col_ptr + 1 = %p\n", (void*)(col_ptr + 1));
// row_ptr+1 比 col_ptr+1 跨越的字节数多3倍(4*sizeof(int) vs sizeof(int))
五、通过指针遍历二维数组
以下是几种常见的遍历方式:
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 方法1:下标法(最直观)
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
}
// 方法2:列指针遍历
for (int *p = arr[0]; p < arr[0] + 12; p++) {
printf("%d ", *p);
}
// 方法3:行指针遍历
for (int (*row)[4] = arr; row < arr + 3; row++) {
for (int *col = *row; col < *row + 4; col++) {
printf("%d ", *col);
}
}
六、函数参数的二维数组
当二维数组作为函数参数传递时,会退化为一维数组的指针:
// 三种等价写法,效果相同
void print_matrix1(int arr[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
}
}
void print_matrix2(int (*arr)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
}
}
// 注意:下面这种写法是错误的!
void print_matrix3(int **arr, int rows, int cols) {
// 这只能接收"指针的指针",不能接收二维数组!
}
二维数组作为函数参数时,第二维的长度必须指定,因为编译器需要知道每行有多少个元素来计算指针偏移。
七、总结
- 二维数组本质:内存中连续存储的一维数组,按行优先排列
- 数组名退化:在表达式中退化为"指向第一行的指针"(类型为 int(*)[4])
- 数组指针:int(*p)[4],p+1 跨越一整行(4个元素)
- 指针数组:int *ap[4],ap 是数组,元素类型为 int*
- 函数参数:二维数组参数必须指定第二维长度