-
什么是指针数组:
int* arr[3] -
数组名什么时候退化为指针
-
arr[i][j]的本质:*(*(arr + i) + j) -
指针数组模拟二维数组 vs 真正二维数组
-
内存布局差异:连续 vs 不保证连续
-
sizeof(arr)、sizeof(arr[0])、sizeof(arr1)的区别 -
指针数组作为函数参数时退化为二级指针
-
为什么
int**不能直接接收int a[3][5] -
二级指针的类型链条:
a -> p -> pp -
二级指针的解引用层级:
*pp、**pp -
二级指针的常见用途:修改一级指针、动态二维数组
-
%p打印指针的注意点 -
多级指针、指针运算、越界风险
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]。
arr1、arr2、arr3 都是 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 个指针。 -
arr1、arr2、arr3各自连续。 -
但
arr1、arr2、arr3之间不保证连续。 -
真正二维数组
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 + 1、printf("%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);
很多编译器不转换也能跑,但严格写法更安全。
另外,*pp 和 p 的值相同,都是 a 的地址;pp 和 &p 的值相同,都是 p 的地址。但它们的"存储位置"不同,意义也不同。
小结
-
int* arr[3]是指针数组,不是二维数组。 -
arr[i][j]本质是*(*(arr + i) + j)。 -
指针数组模拟二维数组:行之间不一定连续,行长度可以不同。
-
真正二维数组:整体连续,类型退化为
int(*)[N]。 -
int**和int(*)[N]完全不同,不能混用。 -
二级指针保存一级指针的地址,
*pp是一级指针,**pp是最终变量。 -
二级指针常用于修改一级指针的指向。