C 语言二维数组与指针深度解析

引言

二维数组是 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*
  • 函数参数:二维数组参数必须指定第二维长度