C语言指针进阶——指针数组模拟二维数组,以及二级指针到底怎么理解

  1. 什么是指针数组:int* arr[3]

  2. 数组名什么时候退化为指针

  3. arr[i][j] 的本质:*(*(arr + i) + j)

  4. 指针数组模拟二维数组 vs 真正二维数组

  5. 内存布局差异:连续 vs 不保证连续

  6. sizeof(arr)sizeof(arr[0])sizeof(arr1) 的区别

  7. 指针数组作为函数参数时退化为二级指针

  8. 为什么 int** 不能直接接收 int a[3][5]

  9. 二级指针的类型链条:a -> p -> pp

  10. 二级指针的解引用层级:*pp**pp

  11. 二级指针的常见用途:修改一级指针、动态二维数组

  12. %p 打印指针的注意点

  13. 多级指针、指针运算、越界风险

1. 指针数组模拟二维数组

你的代码:

c

复制代码
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
int* arr[3] = {arr1, arr2, arr3};

for (i = 0; i < 3; i++)
{
    for (j = 0; j < 5; j++)
    {
        printf("%d ", arr[i][j]);
    }
    printf("\n");
}

这里 arr 是"指针数组",不是二维数组。

c

复制代码
int* arr[3];

因为 [] 的优先级比 * 高,所以 arr 先和 [3] 结合:

c

复制代码
arr 是数组,有 3 个元素;
每个元素的类型是 int*;
所以 arr 的类型是 int*[3]。

arr1arr2arr3 都是 int[5] 类型。在初始化 arr 时,数组名 arr1 退化为指向首元素的指针,也就是 int*,所以可以放进 int* arr[3]

arr[i][j] 的本质是:

c

复制代码
arr[i][j] == *(*(arr + i) + j)

拆开看:

c

复制代码
arr[i]      // 第 i 个指针,类型 int*
arr[i][j]   // 第 i 个指针指向的数组的第 j 个元素,类型 int

所以它确实能模拟出二维数组的访问效果。


2. 它和真正二维数组的区别

真正二维数组:

c

复制代码
int a[3][5];

内存里通常是连续的 15 个 int

text

复制代码
a[0][0] a[0][1] ... a[0][4] a[1][0] ... a[2][4]

而你的写法:

c

复制代码
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
int* arr[3] = {arr1, arr2, arr3};

内存大致是:

text

复制代码
arr:   [ &arr1[0] ][ &arr2[0] ][ &arr3[0] ]
              |            |            |
              v            v            v
arr1:      1 2 3 4 5
arr2:      2 3 4 5 6
arr3:      3 4 5 6 7

注意:

  • arr 本身是连续的,里面存了 3 个指针。

  • arr1arr2arr3 各自连续。

  • arr1arr2arr3 之间不保证连续。

  • 真正二维数组 int a[3][5] 的所有元素整体连续。

所以指针数组模拟二维数组更灵活,比如可以实现"锯齿数组":

c

复制代码
int row1[] = {1, 2};
int row2[] = {3, 4, 5, 6};
int row3[] = {7};
int* arr[3] = {row1, row2, row3};

但真正二维数组每行长度必须一致。


3. sizeof 陷阱

c

复制代码
int arr1[] = {1,2,3,4,5};
int* arr[3] = {arr1, arr2, arr3};

在同一个函数内:

c

复制代码
sizeof(arr1); // 20,假设 int 是 4 字节,5 * 4
sizeof(arr);  // 3 * sizeof(int*),64 位下通常是 24
sizeof(arr[0]); // sizeof(int*),64 位下通常是 8
sizeof(*arr);   // sizeof(int*),也是 8

不要以为:

c

复制代码
sizeof(arr) == 3 * 5 * sizeof(int)

不是。因为 arr 不是二维数组,它只是"存了 3 个指针的数组"。

另外,数组名在大多数表达式中会退化为首元素指针,但有两个常见例外:

c

复制代码
sizeof(arr1); // 不退化,得到整个数组大小
&arr1;        // 不退化,得到数组指针 int(*)[5]

其他像函数传参、arr1 + 1printf("%p", arr1) 等,基本都会退化。


4. 指针数组作为函数参数

c

复制代码
void print(int** p, int rows, int cols)
{
    for (int i = 0; i < rows; i++)
    {
        for (int j = 0; j < cols; j++)
        {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }
}

调用:

c

复制代码
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
int* arr[3] = {arr1, arr2, arr3};

print(arr, 3, 5);

这里 arr 作为实参时,退化为 int**,因为它的首元素是 int*,指向首元素的指针就是 int**

但是注意:真正二维数组不能这样传:

c

复制代码
int a[3][5];
print(a, 3, 5); // 错误

因为 a 作为表达式会退化为 int(*)[5],也就是"指向有 5 个 int 的数组的指针",不是 int**。真正二维数组应该这样写:

c

复制代码
void print2(int (*p)[5], int rows)
{
    for (int i = 0; i < rows; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            printf("%d ", p[i][j]);
        }
        printf("\n");
    }
}

int a[3][5];
print2(a, 3);

所以:

c

复制代码
int**      // 可以接收指针数组名
int(*)[5]  // 可以接收 int[3][5] 这样的二维数组名

二者不是一回事。


5. 二级指针

你的代码:

c

复制代码
int a = 100;
int* p = &a;
int** pp = &p;

printf("a的地址为:%p\n", *pp);
printf("a的地址为:%p\n", p);

printf("p的地址为:%p\n", pp);
printf("p的地址为:%p\n", &p);

printf("a=%d", **pp);

类型链条:

text

复制代码
a   是 int
&a  是 int*
p   是 int*,保存了 &a
&p  是 int**,保存了 p 的地址
pp  是 int**,保存了 &p
*pp 是 p,类型 int*
**pp 是 a,类型 int

可以画成:

text

复制代码
pp  --->  p  --->  a
        &p       &a      100

所以:

c

复制代码
*pp  == p  == &a
**pp == *p == a == 100
pp   == &p
&p   == pp

二级指针最常见的用途是:在函数内部修改一级指针的指向。

c

复制代码
void change(int** pp, int* newAddr)
{
    *pp = newAddr;
}

int a = 10;
int b = 20;
int* p = &a;
change(&p, &b);
// 现在 p 指向 b

这里传入 &p,类型是 int**,函数内部 *pp = newAddr 就修改了外面的 p


6. 指针打印的注意点

printf%p 严格来说要求参数是 void*。更规范的写法是:

c

复制代码
printf("a的地址为:%p\n", (void*)*pp);
printf("a的地址为:%p\n", (void*)p);
printf("p的地址为:%p\n", (void*)pp);
printf("p的地址为:%p\n", (void*)&p);

很多编译器不转换也能跑,但严格写法更安全。

另外,*ppp 的值相同,都是 a 的地址;pp&p 的值相同,都是 p 的地址。但它们的"存储位置"不同,意义也不同。


小结

  • int* arr[3] 是指针数组,不是二维数组。

  • arr[i][j] 本质是 *(*(arr + i) + j)

  • 指针数组模拟二维数组:行之间不一定连续,行长度可以不同。

  • 真正二维数组:整体连续,类型退化为 int(*)[N]

  • int**int(*)[N] 完全不同,不能混用。

  • 二级指针保存一级指针的地址,*pp 是一级指针,**pp 是最终变量。

  • 二级指针常用于修改一级指针的指向。

相关推荐
山下梅子酒2252 小时前
洛谷-入门-B2049
c语言
剑指offer.4 小时前
嵌入式硬件-ARM芯片的启动
c语言·嵌入式硬件·嵌入式
Logic1015 小时前
C语言/数据结构动态规划题解:Kadane算法求最大子数组和——O(n)时间O(1)空间
c语言·数据结构·动态规划·贪心·时间复杂度·算法题·最大子数组和
formulahendry5 小时前
微信和 VS Code 强强联合!WeChat AHP 来了
visual studio code·vs code
雷✘6 小时前
C 程序从源文件到运行:预处理、编译、汇编、链接重定位与执行环境
c语言
Logic1016 小时前
C语言/数据结构位运算题解:异或XOR找出数据表中的“独特编号“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
endeavor127 小时前
BootLoader与OTA学习(三)
c语言·stm32
布莱克6058 小时前
理解内存泄漏:成因、发现方法与解决策略(C++ 举例)
c语言·c++·内存泄漏
Logic1019 小时前
C语言/数据结构位运算题解:异或XOR找出英雄升级系统中的“独特经验值“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质