前言
指针是C语言的灵魂,也是让无数初学者"从入门到放弃"的罪魁祸首。但说实话,指针本身并不复杂------复杂的是它背后所代表的内存地址这一抽象概念。
一旦你理解了"指针就是一个存放地址的变量",你会发现指针不仅不恐怖,反而是C语言最优雅的设计之一。
今天,我们从最基础的一级指针开始,一路走到函数指针和复杂声明解析,帮你彻底拿下指针。
一、指针的本质:地址的容器
```c
int a = 42;
int *p = &a;
```
在内存中,变量 a 占据4个字节(假设32位系统),这4个字节的起始地址就是 &a。而指针变量 p 存储的就是这个地址。
关键理解:
· p 存的是地址(如 0x7ffd1234)
· *p 是"解引用",意思是"去 p 存储的地址那里,取出该地址上的值"
画个图就清晰了:
```
内存地址: 0x7ffd1230 0x7ffd1234 0x7ffd1238
+----------+----------+----------+
| ? | 42 | ? |
+----------+----------+----------+
↑
a 在这里
p = 0x7ffd1234,p自己也有自己的地址
```
二、一级指针:最基础的指针
常见用法
```c
// 1. 修改外部变量(函数内修改函数外的值)
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
// 2. 返回多个结果
int divide(int a, int b, int *remainder) {
if (b == 0) return 0;
*remainder = a % b;
return a / b;
}
// 3. 避免大结构体拷贝(传指针比传值高效)
void process_struct(struct BigData *data) {
// 只传8字节的指针,而非整个结构体
}
```
空指针与野指针
类型 定义 后果
空指针 int *p = NULL; 解引用会段错误(至少能发现)
野指针 int *p; 未初始化 解引用可能修改随机内存,极难调试
建议:声明指针时立即初始化,无法确定值时初始化为 NULL。
三、二级指针:指向指针的指针
二级指针常用于需要在函数内修改指针本身的场景。
```c
// 场景:动态分配内存后,返回给调用者
void allocate_memory(int **ptr, size_t size) {
*ptr = (int*)malloc(size * sizeof(int));
// 调用者传的是 &p,所以 *ptr 就是调用者的 p
}
int main() {
int *p = NULL;
allocate_memory(&p, 10); // 传入指针的地址
p[0] = 100;
free(p);
return 0;
}
```
为什么要用二级指针? C语言是值传递。在函数内修改 p 本身(而非 *p),必须传递 &p。
四、指针与数组:暧昧不清的关系
数组名就是指针吗?
不是。数组名在大多数表达式中会被隐式转换为指向首元素的指针,但有例外:
```c
int arr[5] = {1,2,3,4,5};
int *p = arr; // arr 转换为 &arr[0]
printf("%zu\n", sizeof(arr)); // 20 = 5*4,数组名没转换
printf("%zu\n", sizeof(p)); // 8(64位系统),指针的大小
```
指针运算
```c
int *p = arr;
p++; // 移动 sizeof(int) = 4 字节
*(p+2); // 等价于 arr[3]
```
公式:arr[i] 完全等价于 *(arr + i)
数组作为函数参数:秘密就是指针
```c
void func(int arr[]) { // 编译器自动改写为 int *arr
printf("%zu", sizeof(arr)); // 输出 8,不是20!
}
```
记住:数组形参本质上就是指针,所以无法在函数内获取数组长度,需要额外传入 size。
五、指针数组 vs 数组指针
这是面试常考题,区分关键在于优先级:[] 优先级高于 *。
写法 含义 记忆技巧
int *p[5] 指针数组:5个元素,每个都是 int* p 先和 [] 结合
int (*p)[5] 数组指针:指向一个长度为5的int数组 (*p) 表示 p 是指针
```c
// 指针数组:常用于字符串数组
char *strs[] = {"hello", "world", "c"};
// 数组指针:常用于二维数组
int arr[3][5];
int (*p)[5] = arr; // p 指向第一行
```
六、函数指针:把函数当作数据
函数也有地址,可以存储在指针中,实现回调、策略模式等。
```c
// 声明一个函数指针:指向 返回int、参数(int,int) 的函数
int (*p)(int, int);
// 赋值
int add(int a, int b) { return a + b; }
p = add;
// 调用
int result = p(3, 5); // 等价于 add(3,5)
```
实用场景:回调函数
```c
// 通用的排序函数,让用户决定比较规则
void sort(int *arr, int size, int (*cmp)(int, int)) {
for (int i = 0; i < size; i++) {
for (int j = i+1; j < size; j++) {
if (cmp(arr[i], arr[j]) > 0) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
}
int asc(int a, int b) { return a - b; }
int desc(int a, int b) { return b - a; }
// 使用
sort(myarr, 10, asc); // 升序
sort(myarr, 10, desc); // 降序
```
函数指针的复杂声明(读懂即可)
```c
// 信号处理函数:void (*signal(int sig, void (*handler)(int)))(int)
// 用 typedef 简化
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);
```
七、const 与指针:四种组合
这是初学者最容易混淆的地方,记住一条规则:const 修饰谁,谁就不能改。
写法 含义 能否改指向 能否改值
const int *p 指向常量的指针 ✅ 可以 ❌ 不行
int const *p 同上 ✅ 可以 ❌ 不行
int * const p 常量指针 ❌ 不行 ✅ 可以
const int * const p 两者都不可改 ❌ 不行 ❌ 不行
记忆口诀:const 在 * 左边修饰值,在 * 右边修饰指针本身。
八、常见陷阱与调试技巧
- 解引用未初始化的指针
```c
int *p;
*p = 10; // 错误!p 指向哪里?
```
- 返回局部变量的地址
```c
int* bug() {
int a = 10;
return &a; // 错误!函数返回后 a 被销毁
}
```
- 释放后继续使用
```c
free(p);
*p = 10; // 未定义行为
p = NULL; // 释放后立即置空是个好习惯
```
- 调试技巧
· 打印指针值:printf("p = %p\n", p);
· 使用 Valgrind 检测非法内存访问
· 地址消毒剂:gcc -fsanitize=address -g
结语
指针确实需要时间去消化,但一旦掌握了它,你就真正拥有了C语言。记住三个核心概念:
-
指针就是地址
-
解引用就是去那个地址取值
-
传指针是为了让函数能够修改外部变量
剩下的,无非是反复练习、反复调试。下一篇文章,我们将进入 C语言内存布局,看看程序在运行时的代码、数据、堆、栈是如何组织的。
下一篇预告:《C语言内存全景图:从代码到运行的完整旅程》
有任何指针方面的问题,欢迎在评论区留言讨论!