指针与变量:从基础到进阶
指针这东西,说白了就是地址。我刚开始学C语言时,总觉得指针很神秘,像是某种黑魔法。后来做嵌入式开发,调试过几次野指针导致的死机后,才真正理解了它。嗯,今天我们就从最基础的开始,一步步把指针讲透。
一、指针指向普通变量
每个变量在内存中都有个地址。指针变量就是用来存这个地址的。你可以把它想象成门牌号——变量是房子,指针就是写着地址的小纸条。
int a = 10; // 普通变量
int *p = &a; // 指针p指向变量a的地址
这里有个关键点:&是取地址运算符,*是解引用运算符。我个人习惯把int *p读作「p是一个指向int的指针」。声明时写int* p还是int *p?其实都可以,我更喜欢后者,因为*p本身就是一个int,语义更清晰。
核心理解:
p存的是地址(比如 0x7fff1234)*p是取这个地址里的值(也就是 10)&a是变量a的地址
二、通过指针修改变量值
指针最大的用处之一,就是间接修改变量。你想想看,函数传参时如果传的是值,改的是副本;传指针,才能改原件。
void change_value(int *p) {
*p = 100; // 通过指针修改原变量
}
int main() {
int a = 10;
change_value(&a);
printf("%d\n", a); // 输出 100
return 0;
}
我在项目中遇到过一个问题:有个同事在中断服务函数里直接修改全局变量,结果数据总是不对。后来发现是编译器优化导致的——变量被优化到了寄存器里。解决办法就是传指针,用volatile修饰。嗯,这是题外话,但说明指针在底层开发中真的很重要。
避坑指南:我曾经在写链表操作时,忘记对空指针做判断,结果程序跑飞了。记住:使用指针前,一定要检查是否为NULL。
三、指针的指针(二级指针)
二级指针,就是指向指针的指针。为什么要用二级指针?最常见的场景是:你想在函数里修改传入的指针本身,而不仅仅是指针指向的值。
void alloc_memory(int **pp) {
*pp = (int *)malloc(sizeof(int));
**pp = 42;
}
int main() {
int *p = NULL;
alloc_memory(&p); // 传的是指针的地址
printf("%d\n", *p); // 输出 42
free(p);
return 0;
}
你看,int **pp 读作「pp是一个指向int指针的指针」。*pp 得到的是p本身(一个int*),**pp 才是最终的值。说白了,就是多了一层间接引用。
我个人习惯在链表、树等数据结构中大量使用二级指针。比如删除链表节点时,用二级指针可以避免处理头节点的特殊情况,代码更简洁。
注意:二级指针容易让人晕。我建议你画个内存图,把变量、指针、二级指针的关系画清楚。纸上画一遍,比看十遍代码都管用。
四、void* 泛型指针
void* 是C语言里的「万能指针」。它可以指向任何类型的数据,但不能直接解引用——因为编译器不知道它指向的是什么类型。
int a = 10;
double b = 3.14;
void *vp;
vp = &a; // 指向int
// printf("%d\n", *vp); // 错误!不能直接解引用
printf("%d\n", *(int*)vp); // 先转成int*再解引用
vp = &b; // 指向double
printf("%f\n", *(double*)vp); // 先转成double*
void* 最常见的用途是实现泛型函数。比如标准库里的memcpy、qsort,参数都是void*。我在写通用链表、通用队列时也经常用。
关键点:
- void* 可以接收任何类型的指针
- 使用前必须强制转换成具体类型
- 不能对void*做指针运算(比如 p++)
为什么会这样?因为指针运算依赖于类型大小。void* 不知道指向的对象有多大,自然没法做加减。你想想看,如果不知道每个元素占几个字节,p++该跳多远?
五、知识体系总览
下面这张图总结了本章的核心内容。我建议你把它保存下来,复习时看一眼就能回忆起全貌。
六、总结与建议
指针这东西,光看是学不会的。我建议你打开编译器,把上面的代码都敲一遍。改改值,打印地址,看看内存里到底发生了什么。
记住几个要点:
- 指针变量本身也有地址(可以用&取)
- 声明时的
*和使用的*含义不同——声明时表示「这是一个指针」,使用时表示「解引用」 - 二级指针在需要修改指针本身时用,比如动态分配内存
- void* 是工具,用好了能写出通用代码,用不好容易出bug
嗯,今天就到这里。下一章我们会聊指针和数组的关系——这两个东西在C语言里纠缠不清,搞懂了它们,你的C语言水平会上一个大台阶。