C语言中的指针是其灵魂,但也让无数人感到困惑。本篇博客将带你深入理解指针学习中的四大核心难点:
- 彻底搞懂数名 的两个特殊例外(
sizeof和&操作),认清数组名的"双重身份"。 - 掌握指针访问数组的原理,理解下标访问和指针解引用间的等价关系。
- 揭示一维数组传参 时发生的"指针退化"本质,解释
sizeof为何在函数内部失效。 - 详细解析二级指针的原理与解引用操作,学会间接控制一级指针。
读完本文,你将能清晰掌握指针与数组之间的微妙关系,为后续学习数组指针、函数指针打下坚实基础!
目录
- 一、数组名的双重身份
-
- [1.1 数组名的"通用"身份:数组首元素的地址](#1.1 数组名的“通用”身份:数组首元素的地址)
- [1.2 数组名的"特殊"身份:不可忽略的两个例外](#1.2 数组名的“特殊”身份:不可忽略的两个例外)
- 二、使用指针访问数组:更灵活、更强大
-
- [2.1 核心等价关系:下标与指针的转换](#2.1 核心等价关系:下标与指针的转换)
- [2.2 利用指针进行数组遍历](#2.2 利用指针进行数组遍历)
- 三、一维数组传参的本质:传递的是地址,而非整个数组!
-
- [3.1 传参的真相:数组退化为指针](#3.1 传参的真相:数组退化为指针)
- [3.2 函数形参的两种写法与本质](#3.2 函数形参的两种写法与本质)
- [3.3 sizeof 失效的根本原因](#3.3 sizeof 失效的根本原因)
- 四、二级指针:管理指针的指针
-
- [4.1 什么是二级指针?](#4.1 什么是二级指针?)
- [4.2 二级指针的解引用与运算](#4.2 二级指针的解引用与运算)
一、数组名的双重身份
C语言中的数组名 是一个极易令人混淆的概念,它既可以被视为一个指针 (地址),又可以代表整个数组实体。理解数组名,就抓住了C语言指针和数组关系的核心。
1.1 数组名的"通用"身份:数组首元素的地址
在C语言中,这是数组名最常见的用法,也是我们学习指针时被告知的默认规则 。
默认规则:数组名就是数组首元素的地址:
在绝大多数表达式中,当我们使用一个数组名时(例如 arr),它会自动被编译器解释为数组第一个元素的地址 。
这意味着:arr 的值与 &arr[0] 的值是完全相同的。
代码验证:
c
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
printf("数组首元素地址: %p\n", &arr[0]);
printf("数组名arr的值: %p\n", arr);
return 0;
}
运行结果 (地址值是示例,每次分配都有差异,但两者值相等):

总结: arr 的类型是一个指向整型(int)的指针(即 int*)。有了这个地址,我们就可以使用指针方式 *(arr + i) 来访问数组的任意元素。
1.2 数组名的"特殊"身份:不可忽略的两个例外
正是以下这两种特殊情况,让数组名区别于普通的指针变量,从而可以进行一些特殊的运算。
例外一: sizeof(数组名) --- 计算整个数组大小
当数组名作为 sizeof() 运算符的唯一操作数时,它会"回归"到数组的本质,代表整个数组实体。
此时,sizeof(arr) 计算的是整个数组所占用的总字节数,而不是一个地址(指针)的大小。
示例:
c
int arr[10]; // 包含 10 个 int 元素的数组
size_t size_of_arr = sizeof(arr);
size_t size_of_element = sizeof(arr[0]);
printf("整个数组的大小 (sizeof(arr)) = %zu 字节\n", size_of_arr);
printf("首元素的大小 (sizeof(arr[0])) = %zu 字节\n", size_of_element);
printf("数组元素的个数 = %zu\n", size_of_arr / size_of_element);

提示: 这是在函数外部唯一能够直接计算出数组元素个数的方法。如果你在函数内部对作为形参传入的数组使用 sizeof(),它将退化为指针,计算的是指针变量的大小(4或8字节)。
例外二: &数组名 --- 获取整个数组的地址
当对数组名使用取地址运算符 & 时,&arr 也表示整个数组。它取出的是整个数组的地址。
关键区别: 类型和步长
虽然 &arr 和 arr 的值(地址值)是相同的,都指向数组的起始位置,但它们的类型和指针运算规则是完全不同的:
| 表达式 | 类型 (Data Type) | 指针 + 1 的步长 (Offset) |
|---|---|---|
arr |
int* (指向一个 int 元素的指针) |
移动一个 int 元素的大小(4字节) |
&arr |
int(*)[10] (指向一个有 10 个 int 元素的数组的指针,即数组指针类型) |
移动整个数组的大小(40字节) |
代码演示它们在指针运算上的本质区别:
c
int main()
{
int arr[10] = { 0 }; // 假设 int = 4字节
printf("arr 的值: %p\n", arr);
printf("arr + 1 的值: %p\n", arr + 1); // arr + 1 跳过一个 int (4字节)
printf("\n&arr 的值: %p\n", &arr);
printf("&arr + 1 的值: %p\n", &arr + 1); // &arr + 1 跳过整个数组 (40字节)
return 0;
}
运行结果展示:

这⾥我们发现arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。
到这⾥⼤家应该搞清楚数组名的意义了吧。
数组名是数组⾸元素的地址,但是就只有这两个操作符例外。
二、使用指针访问数组:更灵活、更强大
理解了数组名就是数组首元素的地址,我们就可以使用指针更灵活、更高效地访问数组的元素。
2.1 核心等价关系:下标与指针的转换
在C语言中,数组的下标访问 (arr[i]) 本质上就是通过指针运算来实现的。
对于一个数组 arr 和一个指向其首元素的指针 p (int* p = arr;),以下四种访问方式在编译器眼中是完全等价的:
| 访问方式 | 表达式 | 描述 |
|---|---|---|
| 下标法 (数组名) | arr[i] |
最常用的数组访问方法。 |
| 指针法 (数组名) | *(arr + i) |
数组名作为地址,加上偏移量 i 后解引用。 |
| 下标法 (指针变量) | p[i] |
将指针变量 p 当作数组名来使用,非常灵活。 |
| 指针法 (指针变量) | *(p + i) |
指针变量 p 加上偏移量 i 后解引用。 |
代码验证:
c
int main()
{
int arr[5] = { 10, 20, 30, 40, 50 };
int* p = arr; // p 存储 arr 的首元素地址
int i = 2; // 访问第三个元素(30)
// 1. 下标法 (数组名)
printf("arr[2] = %d\n", arr[i]);
// 2. 指针法 (数组名)
printf("*(arr + 2) = %d\n", *(arr + i));
// 3. 下标法 (指针变量)
printf("p[2] = %d\n", p[i]);
// 4. 指针法 (指针变量)
printf("*(p + 2) = %d\n", *(p + i));
return 0;
}
结果展示:

可以发现将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i)。
同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。
深入理解: 编译器在处理
arr[i]时,会将其悄悄转换为*(arr + i)的指针运算形式。因此,用指针访问数组,只是让这种底层机制显性化。
2.2 利用指针进行数组遍历
使用指针进行数组遍历,是C语言中常见的优化手段,尤其在性能要求较高的场景。
我们可以通过递增指针的方式,依次访问每个元素。
c
int main()
{
int arr[5] = {10, 20, 30, 40, 50};
int num = sizeof(arr) / sizeof(arr[0]); // 元素个数
int* p = arr; // 指向首元素
printf("使用指针遍历数组:\n");
// 遍历循环
for (int i = 0; i < num; i++)
{
// 关键操作:对 p 进行解引用,获取当前元素的值
printf("%d ", *p);
// 关键操作:指针 p 自增,p++ 使得指针跳过一个 int 大小(4字节),指向下一个元素
p++;
}
// 输出:10 20 30 40 50
return 0;
}
通过这种方式,我们可以看到指针在数组操作中的强大灵活性。由于指针自增是按照它所指向的类型(如 int)的大小进行步进的,因此可以精确地指向数组中的下一个元素。
三、一维数组传参的本质:传递的是地址,而非整个数组!
这是C语言初学者最容易产生疑惑的地方之一:为什么我们不能在函数内部通过 sizeof(arr) 来计算传入数组的元素个数?
3.1 传参的真相:数组退化为指针
C语言在进行一维数组传参 时,遵循一个核心原则:形参中的数组名会被编译器自动退化(decay)为一个指向其首元素的指针 。
当你调用一个函数并传入一个数组名时,实际上传递给函数的是数组首元素的地址。
| 数组作为实参 | 数组名 | 传递的是 |
|---|---|---|
int arr[10]; |
arr |
arr (数组首元素的地址) |
因此,在函数内部接收这个地址的形参 ,本质上就是一个指针变量。
3.2 函数形参的两种写法与本质
无论你将函数形参写成数组形式 还是指针形式,它们的本质都是一样的:接收一个地址。
c
void test(int arr[]) // 写法一:数组形式
{
// ...
}
void test_ptr(int* p) // 写法二:指针形式
{
// ...
}
// 在编译器看来,test 函数和 test_ptr 函数的形参是等效的!
3.3 sizeof 失效的根本原因
正是这种"退化"现象,导致了在函数内部无法通过 sizeof 获取数组的原始大小。
让我们看一个经典的错误示例:
c
void print_array_size(int arr[]) // arr[] 在这里是 int* arr 的别名
{
// 假设在 64 位环境下,指针大小是 8 字节
int size_in_func = sizeof(arr);
printf("函数内 sizeof(arr) 的结果: %d 字节\n", size_in_func);
// 这里的 size_in_func 会是 4 或 8(指针大小),而不是 40!
}
int main()
{
int arr[10] = {0}; // 原始数组大小是 10 * 4 = 40 字节
printf("函数外 sizeof(arr) 的结果: %zu 字节\n", sizeof(arr)); // 输出 40
print_array_size(arr);
return 0;
}
结果展示:

结论:
- 在函数外部(定义数组的作用域内),
sizeof(arr)触发例外一,计算整个数组的大小(40字节)。 - 在函数内部,由于数组名
arr退化成了一个指针变量,sizeof(arr)计算的是指针变量自身的大小(4或8字节),与数组的元素个数无关。
因此,如果你需要在函数内部知道数组的元素个数,必须显式地将元素个数作为另一个参数传入。
四、二级指针:管理指针的指针
理解了指针(一级指针)是用来存放变量地址 的,那么二级指针又是什么呢?
4.1 什么是二级指针?
指针变量也是一种变量,既然是变量,它就和普通变量一样,有自己的存储空间 和地址 。
二级指针(Pointer to a Pointer) ,就是用来存放一级指针变量的地址的变量。
| 指针级别 | 变量类型 | 存放内容 | 声明方式 |
|---|---|---|---|
| 普通变量 | int |
实际数据 (e.g., 10) | int a = 10; |
| 一级指针 | int* |
普通变量 a 的地址 |
int *pa = &a; |
| 二级指针 | int** |
一级指针 pa 的地址 |
int **ppa = &pa; |
代码示例:
c
int main()
{
int a = 10; // 1. 普通变量 a,存储数据 10
int* pa = &a; // 2. 一级指针 pa,存储 a 的地址
int** ppa = &pa; // 3. 二级指针 ppa,存储 pa 的地址
printf("变量 a 的值: %d\n", a);
// 我们可以通过三种方式访问变量 a
printf("通过 a 访问: %d\n", a);
printf("通过 *pa 访问: %d\n", *pa);
printf("通过 **ppa 访问: %d\n", **ppa); // 多次解引用
return 0;
}

这里给个图解,便于理解:

4.2 二级指针的解引用与运算
二级指针的强大之处在于它提供了间接修改一级指针的能力。通过两次解引用,我们可以精确地修改最终的数据。
第一次解引用:*ppa 访问一级指针
对二级指针 ppa 进行第一次解引用(*ppa),找到的是它所指向的变量------一级指针 pa 本身。
用途: 可以通过
*ppa来改变pa变量所存储的地址。
c
int a = 10;
int b = 20;
int *pa = &a;
int **ppa = &pa;
// 1. *ppa 等价于 pa
*ppa = &b; // 语句解读:将 b 的地址赋值给 *ppa (即 pa)
// 结果:pa 现在指向 b,而不是 a
printf("现在 *pa 的值: %d\n", *pa); // 输出 20
这在函数传参中非常重要,如果你想在函数内部修改一个指针变量(例如在函数中给 pa 重新分配内存),你就必须传入它的地址,即二级指针。
第二次解引用:**ppa 访问最终数据
对 *ppa(即 pa)再进行一次解引用(**ppa),找到的就是最终的数据------变量 a 本身。
用途: 通过二级指针修改它最终指向的数据的值。
c
int a = 10;
int *pa = &a;
int **ppa = &pa;
// 1. **ppa 等价于 *pa,也等价于 a
**ppa = 30; // 语句解读:将 30 赋值给 **ppa (即 a)
// 结果:a 的值被修改为 30
printf("现在 a 的值: %d\n", a); // 输出 30
二级指针为我们提供了一个通过两层间接访问来控制数据的能力,这是高级C编程中绕不开的关键概念。