C46-二维数组与指针的总结
一 概念明晰:“解引用”与“解引”
1. 什么是“解引用”?
定义:通过指针访问其指向的内存地址中存储的实际数据。
操作符:使用 *
运算符(如 *ptr
)。
核心作用:将指针从“地址”转换为“该地址存储的值”。
示例
int num = 42;
int *ptr = # // ptr 存储 num 的地址// 解引用 ptr 获取 num 的值
printf("%d\n", *ptr); // 输出 42(解引用后得到 num 的值)
2. “解引”是“解引用”的简化表达
3. 为什么需要解引用?
指针的本质是存储内存地址的变量,而程序通常需要操作地址中的实际数据。解引用是连接“地址”和“数据”的关键步骤。
4. 解引用的常见用途
(1)访问动态内存
(2)修改函数外部的变量
(3)遍历数组
int arr[3] = {1, 2, 3};
int *ptr = arr;
for (int i = 0; i < 3; i++) {printf("%d ", *(ptr + i)); // 解引用访问每个元素
}
// 输出:1 2 3
二 概念明晰:隐式转换
在C语言中,隐式转换是指编译器在表达式求值、赋值或函数调用时,自动进行的类型转换,无需程序员显式指定。
1. 隐式转换的常见场景
(1)数组到指针的转换
-
二维数组
a
的隐式转换:当使用数组名a时(如传递给函数或参与指针运算),它会被隐式转换为指向其首元素的指针:
int a[2][3]; // a 的类型本是 int [2][3],但在此处隐式转换为 int (*)[3](指向第一行的指针) func(a); // 等价于 func(&a[0])
三 与二维数组有关的指针
现已定义一个名为’a’的二维数组
(一) a与*a
a
1. a
的本质
a
是一个二维数组的名称。a
的本质是一个指向一维数组的指针(即指向第一行的指针)。
2. a
的基本含义
a
表示二维数组的首地址,即第 0 行的地址。它的值是&a[0]
。a
的指向:a
指向的是整个第一行(一个一维数组),而不是某个具体的元素。
3. a
退化的指针类型
a
退化为int (*)[4]
(假设a
是int a[3][4]
)。- 例外情况:
sizeof(a)
:此时a
不会退化,结果是整个数组的大小(如3 * 4 * sizeof(int)
)。&a
:此时a
不会退化,结果是整个二维数组的地址(类型是int (*)[3][4]
)。
4. a
的用法
- 作为指向行的指针:
a + i
:指向第i
行的指针(即&a[i]
)。*(a + i)
:访问第i
行(即a[i]
,是一个一维数组)。
- 遍历二维数组:
- 可以通过
a
和行索引访问每一行,例如a[i][j]
或*(*(a + i) + j)
。
- 可以通过
*a
1. *a
的本质
*a
是对a
的解引用。由于a
是指向第一行的指针,*a
就是第一行本身(即a[0]
)。a[0]
是一个一维数组的名称,因此*a
的本质是一个指向第一行第一个元素的指针(即&a[0][0]
)。
2. *a
的基本含义
*a
表示二维数组第一行的首地址(即第 0 行第 0 列元素的地址)。*a
的指向:*a
指向的是a[0][0]
,即具体的整数元素。
3. *a
退化的指针类型
*a
是一维数组a[0]
的名称,因此会退化为指向其首元素的指针,即int *
(指向a[0][0]
)。- 例外情况:
sizeof(*a)
:此时*a
不会退化,结果是第一行的大小(如4 * sizeof(int)
)。&*a
:等价于a
,类型是int (*)[4]
。
4. *a
的用法
- 作为指向列元素的指针:
*a + j
:指向第 0 行第j
列元素的指针(即&a[0][j]
)。*(*a + j)
:访问第 0 行第j
列元素(即a[0][j]
)。
- 遍历一维数组:
- 可以通过
*a
访问第一行的元素,例如(*a)[j]
或*(*a + j)
- 可以通过
总结对比
项目 | a | *a |
---|---|---|
本质 | 指向一维数组的指针 (int (*)[4] ) | 指向整数的指针 (int * ) |
基本含义 | 第 0 行的地址 (&a[0] ) | 第 0 行第 0 列的地址 (&a[0][0] ) |
退化类型 | int (*)[4] (通常不退化) | int * (退化为一维数组首元素指针) |
用法 | 访问行(如 a + i ) | 访问列(如 *a + j ) |
(二) a[i]与&a[i]
a[i]
1. a[i]
的本质
a[i]
是二维数组a
的第i
行。在 C 语言中,a[i]
等价于*(a + i)
,即对指向第i
行的指针解引用,得到第i
行本身(一个一维数组)。
2. a[i]
的基本含义
a[i]
表示第i
行的首地址(即第i
行第 0 列元素的地址),等价于&a[i][0]
。a[i]
的指向:a[i]
指向的是第i
行的第一个元素(a[i][0]
)。
3. a[i]
退化的指针类型
- 在大多数表达式中,
a[i]
会退化为指向其首元素的指针,即int *
(指向a[i][0]
)。 - 例外情况:
sizeof(a[i])
:此时a[i]
不会退化,结果是第i
行的大小(如4 * sizeof(int)
)。&a[i]
:此时a[i]
不会退化,结果是第i
行的地址(类型是int (*)[4]
)。
4. a[i]
的用法
-
作为指向列元素的指针:
a[i] + j
:指向第i
行第j
列元素的指针(即&a[i][j]
)。*(a[i] + j)
:访问第i
行第j
列元素(即a[i][j]
)。
-
遍历第
i
行:- 可以通过
a[i]
访问第i
行的元素,例如a[i][j]
或*(a[i] + j)
。
- 可以通过
&a[i]
1. &a[i]
的本质
&a[i]
是取第i
行的地址。由于a[i]
是一个一维数组,&a[i]
的类型是指向一维数组的指针(即int (*)[4]
)。&a[i]
等价于a + i
,即指向第i
行的指针。
2. &a[i]
的基本含义
&a[i]
表示第i
行的地址,即&a[i][0]
的“行视角”地址。&a[i]
的指向:&a[i]
指向的是整个第i
行(一个一维数组),而不是某个具体的元素。
3. &a[i]
退化的指针类型
&a[i]
不会退化,因为它本身就是取地址操作,类型固定为int (*)[4]
(假设a
是int a[3][4]
)。
4. &a[i]
的用法
- 作为指向行的指针:
&a[i] + 1
:指向第i+1
行的指针(即a + i + 1
)。*(&a[i] + 1)
:访问第i+1
行(即a[i + 1]
)。
- 遍历二维数组:
- 可以通过
&a[i]
访问行,例如*(&a[i])[j]
或*(*(&a[i] + 0) + j)
。
- 可以通过
总结对比
项目 | a[i] | &a[i] |
---|---|---|
本质 | 第 i 行的一维数组 (int [4] ) | 指向第 i 行的指针 (int (*)[4] ) |
基本含义 | 第 i 行首元素的地址 (&a[i][0] ) | 第 i 行的地址 (a + i ) |
退化类型 | int * (退化为一维数组首元素指针) | 不退化(固定为 int (*)[4] ) |
用法 | 访问第 i 行的元素(如 a[i][j] ) | 访问行(如 &a[i] + 1 ) |
(三) a+1与a[i]+1
a+1
1.. a + 1
的本质
a + 1
是二维数组a
的指针运算,表示跳过一行(即指向下一行的指针)。- 如果
a
是int a[3][4]
,则a
的类型是int (*)[4]
(指向一维数组的指针),a + 1
会移动sizeof(int[4])
(即4 * sizeof(int)
)字节。
2. a + 1
的基本含义
a + 1
表示第 1 行的地址(即&a[1]
)。- 它仍然是一个指向行的指针(
int (*)[4]
),而不是指向单个元素的指针。
3. a + 1
的退化情况
a + 1
不会退化,因为它本身就是指针运算,类型仍然是int (*)[4]
。- 例外情况:
- **如果解引用
*(a + 1)
,则退化为int *
(**指向a[1][0]
的指针)。
- **如果解引用
4. a + 1
的用法
-
用于遍历二维数组的行:
for (int i = 0; i < 3; i++) {int *row = *(a + i); // 获取第 i 行的首地址for (int j = 0; j < 4; j++) {printf("%d ", row[j]); // 访问 a[i][j]} }
-
直接访问下一行:
int (*next_row)[4] = a + 1; // 指向第 1 行的指针 printf("%d\n", (*next_row)[2]); // 输出 a[1][2]
a[i]+1
1. a[i] + 1
的本质
a[i]
是第i
行的首地址(&a[i][0]
),a[i] + 1
是指向第i
行第 1 个元素的指针(即&a[i][1]
)。- 如果
a
是int a[3][4]
,a[i]
的类型是int *
(指向int
的指针),a[i] + 1
会移动sizeof(int)
字节。
2. a[i] + 1
的基本含义
a[i] + 1
表示第i
行第 1 列元素的地址(即&a[i][1]
)。- 它是一个指向单个元素的指针(
int *
),而不是指向整行的指针。
3. a[i] + 1
的退化情况
a[i]
已经退化成了int *
,所以a[i] + 1
仍然是int *
,没有额外的退化。- 例外情况:
sizeof(a[i] + 1)
仍然是指针大小(通常是 4 或 8 字节),不会退化。
4. a[i] + 1
的用法
-
用于遍历第
i
行的元素:for (int j = 0; j < 4; j++) {printf("%d ", *(a[i] + j)); // 等价于 a[i][j] }
-
直接访问第
i
行的下一个元素:int *next_element = a[i] + 1; printf("%d\n", *next_element); // 输出 a[i][1]
总结对比
项目 | a + 1 | a[i] + 1 |
---|---|---|
本质 | 指向下一行的指针 (int (*)[4] ) | 指向当前行下一元素的指针 (int * ) |
基本含义 | 第 1 行的地址 (&a[1] ) | 第 i 行第 1 列元素的地址 (&a[i][1] ) |
退化情况 | 不退化(仍然是 int (*)[4] ) | 不退化(仍然是 int * ) |
用法 | 遍历行(如 *(a + 1)[j] ) | 遍历列(如 *(a[i] + j) ) |
(四) a[1][2]与&a[1][2]
a[1][2]
1. a[1][2]
的本质
a[1][2]
是二维数组a
的第1
行第2
列的元素(即a
的第2
行第3
列,因为索引从0
开始)。- 它是一个具体的整数值(假设
a
是int
类型数组),而不是指针。
2. a[1][2]
的基本含义
a[1][2]
表示直接访问二维数组a
的第1
行第2
列的元素。- 它是数组元素的直接取值,不涉及指针运算(但底层仍然通过指针访问)。
3. a[1][2]
的退化情况
-
a[1][2]
不会退化,因为它已经是具体的值(int
类型),而不是指针。 -
它可以直接用于赋值、计算或打印:
int val = a[1][2]; // val = 7 printf("%d\n", a[1][2]); // 输出 7
4. a[1][2]
的用法
-
直接访问数组元素:
printf("%d\n", a[1][2]); // 输出 7
-
修改数组元素:
a[1][2] = 100; // 修改 a[1][2] 为 100
-
作为函数参数传递:
void print_val(int x) {printf("%d\n", x); } print_val(a[1][2]); // 输出 7
&a[1][2]
1. &a[1][2]
的本质
&a[1][2]
是取a[1][2]
的地址,即指向a[1][2]
的指针。- 它的类型是
int *
(指向int
的指针)。 - 例如,如果
a[1][2]
的地址是0x1040
,则&a[1][2]
的值是0x1040
。
2. &a[1][2]
的基本含义
&a[1][2]
表示第1
行第2
列元素的地址。- 它是一个指向单个
int
的指针,可以用于指针运算或间接访问。
3. &a[1][2]
的退化情况
-
&a[1][2]
不会退化,因为它本身就是取地址操作,类型固定为int *
。 -
它可以用于指针运算:
int *ptr = &a[1][2]; printf("%d\n", *ptr); // 输出 7 printf("%d\n", *(ptr + 1)); // 输出 a[1][3](8)
4. &a[1][2]
的用法
-
获取元素的地址:
int *p = &a[1][2]; // p 指向 a[1][2]
-
通过指针修改数组:
C*(&a[1][2]) = 100; // 等价于 a[1][2] = 100
-
遍历后续元素:
int *p = &a[1][2]; for (int i = 0; i < 2; i++) {printf("%d ", *(p + i)); // 输出 a[1][2] 和 a[1][3] }
总结对比
项目 | a[1][2] | &a[1][2] |
---|---|---|
本质 | 具体的 int 值(如 7 ) | 指向 a[1][2] 的指针 (int * ) |
基本含义 | 直接访问元素值 | 获取元素地址 |
退化情况 | 不退化(已经是 int ) | 不退化(固定为 int * ) |
用法 | 取值、赋值、计算 | 指针运算、间接访问 |
(五)*(a+1)+2、*(*(a+1)+2)以及*(a[1]+2)
1. *(a + 1) + 2
本质
a + 1
是指向第1
行的指针(类型int (*)[4]
)。*(a + 1)
解引用后得到第1
行(类型int [4]
),但在表达式中会退化为int *
(指向a[1][0]
的指针)。*(a + 1) + 2
是在a[1]
的基础上偏移2
个int
,得到&a[1][2]
(类型int *
)。
基本含义
- 表示第
1
行第2
列元素的地址(即&a[1][2]
)。 - 等价于
a[1] + 2
或&a[1][2]
。
退化情况
*(a + 1)
退化为int *
,*(a + 1) + 2
仍然是int *
(不退化)。
用法
- 用于获取
a[1][2]
的地址:int *p = *(a + 1) + 2; // p = &a[1][2] printf("%d\n", *p); // 输出 a[1][2]
2. *(*(a + 1) + 2)
本质
*(a + 1) + 2
是&a[1][2]
(见上)。*(*(a + 1) + 2)
是对&a[1][2]
解引用,得到a[1][2]
的值(类型int
)。
基本含义
- 表示第
1
行第2
列元素的值(即a[1][2]
)。 - 等价于
a[1][2]
或*(a[1] + 2)
。
退化情况
- 已经是具体的
int
值,不涉及退化。
用法
- 直接访问
a[1][2]
:int val = *(*(a + 1) + 2); // val = a[1][2] printf("%d\n", val);
3. *(a[1] + 2)
本质
a[1]
是第1
行的首地址(类型int [4]
,退化为int *
)。a[1] + 2
是&a[1][2]
(类型int *
)。*(a[1] + 2)
是对&a[1][2]
解引用,得到a[1][2]
(类型int
)。
基本含义
- 表示第
1
行第2
列元素的值(即a[1][2]
)。 - 等价于
*(*(a + 1) + 2)
和a[1][2]
。
退化情况
- 已经是具体的
int
值,不涉及退化。
用法
- 直接访问
a[1][2]
:int val = *(a[1] + 2); // val = a[1][2] printf("%d\n", val);
总结对比
表达式 | 类型 | 等价形式 | 含义 |
---|---|---|---|
*(a + 1) + 2 | int * | a[1] + 2 | 第 1 行第 2 列地址 |
*(*(a + 1) + 2) | int | a[1][2] | 第 1 行第 2 列的值 |
*(a[1] + 2) | int | *(*(a + 1) + 2) | 第 1 行第 2 列的值 |
(六) 大总结与对比
1. 核心概念分层
层级 | 典型表达式 | 本质 | 类型 | 退化规则 |
---|---|---|---|---|
数组层 | a | 二维数组首地址 | int [3][4] | →int (*)[4] |
行指针层 | a+i , &a[i] | 第i行地址 | int (*)[4] | 不退化 |
列指针层 | *(a+i) , a[i] | 第i行首元素地址 | int * | 已退化 |
元素层 | a[i][j] | 具体元素值 | int | 不适用 |
2. 关键表达式全解
表达式 | 等价形式 | 作用 | 内存操作说明 |
---|---|---|---|
a | &a[0] | 获取第0行地址 | 步长=一行大小(16字节) |
*a | a[0] | 获取第0行首元素地址 | 步长=1元素大小(4字节) |
a+1 | &a[1] | 移动到第1行地址 | 地址+16字节 |
*(a+1)+2 | &a[1][2] | 获取第1行第2列地址 | 基础地址+8字节 |
*(*(a+1)+2) | a[1][2] | 获取第1行第2列的值 | 直接内存访问 |
&a[1][2] | *(a+1)+2 | 获取元素地址 | 可用于指针运算 |
3. 典型场景对照表
访问需求 | 推荐写法 | 指针写法 | 底层解释 |
---|---|---|---|
遍历所有行 | for(i=0;i<3;i++) | for(p=a;p<a+3;p++) | p每次+16字节 |
遍历某行元素 | for(j=0;j<4;j++) | for(q=a[i];q<a[i]+4;q++) | q每次+4字节 |
随机访问元素 | a[i][j] | *(*(a+i)+j) | 编译器优化后效率相同 |
4. 内存模型可视化
TEXT地址示例 | 值 | 表达式表示
0x1000 | a[0][0]=1 | *(*a)
0x1004 | a[0][1]=2 | *(*a+1)
...
0x1010 | a[1][0]=5 | *(*(a+1))
0x1014 | a[1][1]=6 | *(*(a+1)+1)
0x1018 | a[1][2]=7 | *(*(a+1)+2)
...
0x1020 | a[2][0]=9 | *(*(a+2))
5. 易错点警示
-
维度混淆:
a+1
移动一行,a[0]+1
移动一列 -
解引用陷阱:
*a
得到的是列指针,不是元素值**a
才是首个元素值
-
sizeof差异:
sizeof(a) // 48字节(整个数组) sizeof(a[0]) // 16字节(一行) sizeof(*a) // 16字节(同上一行)
6. 终极理解技巧
- 类型观察法:
- 看到
int (*)[4]
→行指针 - 看到
int *
→列指针
- 看到
- 星号(*)法则:
- 每出现一个
*
抵消一个[]
a[i][j]
≡*(*(a+i)+j)
- 每出现一个
- 地址算术口诀:
- “行跳行大小,列跳元素大小”