4. 指针的深度应用:指针基础、指针与数组、函数指针、指针的指针、const指针保护
指针这东西,说实话,是C语言的灵魂,也是很多嵌入式工程师的噩梦。我见过不少写了三五年代码的人,一碰到指针就发怵。其实没那么玄乎,你把它想成是「门牌号」就对了。变量是房子,指针就是那个门牌号。今天咱们就把指针这层窗户纸捅破。
4.1 指针基础:别怕,就是个地址
指针变量存的是什么?是另一个变量的地址。就这么简单。32位MCU上,指针占4个字节;64位上占8个字节。不管它指向的是char、int还是结构体,指针本身的大小是固定的。
// 基础声明
int a = 10;
int *p = &a; // p 存的是 a 的地址
// 解引用
*p = 20; // 等价于 a = 20
// 指针的大小
printf("指针大小: %zu 字节\n", sizeof(p)); // 在32位MCU上输出4
我的习惯:声明指针时,我总喜欢把星号紧挨着变量名,比如 int *p。这样读起来就是「p是一个指针,指向int」。别写成 int* p,因为 int* p, q 这种写法会让人误以为q也是指针——其实q只是个int。
为什么强调这个?我在项目中遇到过一位同事,因为指针声明风格不统一,代码审查时花了半小时才找出一个bug。嗯,细节决定成败。
4.2 指针与数组:其实是一回事
数组名是什么?是数组首元素的地址。说白了,arr 和 &arr[0] 是同一个东西。但要注意,arr 是个常量指针,你不能给它赋值。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p 指向 arr[0]
// 以下两种写法完全等价
printf("%d\n", arr[2]); // 输出 3
printf("%d\n", *(p + 2)); // 输出 3
// 指针运算
p++; // p 现在指向 arr[1]
printf("%d\n", *p); // 输出 2
你想想看,为什么 p + 2 跳过了8个字节(int占4字节)?因为指针的加减运算,是以「指向的类型大小」为步长的。这就是指针的「类型」存在的意义。
我曾经踩过的坑:用 sizeof(arr) 和 sizeof(p) 搞混了。sizeof(arr) 返回整个数组的大小(20字节),而 sizeof(p) 只返回指针本身的大小(4字节)。把数组名当指针传给函数后,在函数里用 sizeof 就得不到数组长度了。所以,我建议:传递数组时,一定要把长度也传过去。
4.3 函数指针:把函数当参数传
函数指针,说白了就是指向函数的指针。MCU开发中,这东西太常用了——中断回调、状态机、协议栈解析,哪哪都有它。
// 声明一个函数指针
// 指向:返回值是int,参数是两个int的函数
int (*func_ptr)(int, int);
// 定义一个普通函数
int add(int a, int b) {
return a + b;
}
// 让指针指向这个函数
func_ptr = add;
// 通过指针调用函数
int result = func_ptr(3, 4); // result = 7
我个人习惯用 typedef 来简化函数指针的声明,不然那语法看着真头疼:
// 用typedef定义函数指针类型
typedef int (*operation_t)(int, int);
// 现在声明就清爽多了
operation_t op = add;
int r = op(5, 6); // r = 11
实际项目中的应用:我在做一款传感器驱动时,用函数指针数组实现了「命令分发表」。不同的命令码对应不同的处理函数,代码从一堆switch-case变成了一个数组查表。执行效率更高,代码也更优雅。
4.4 指针的指针:二级指针
指针的指针,就是指向指针的指针。什么时候用?当你需要在函数里修改指针本身的值时。
void allocate_memory(int **pp, int size) {
*pp = (int *)malloc(size * sizeof(int));
if (*pp == NULL) {
// 处理分配失败
}
}
int main() {
int *p = NULL;
allocate_memory(&p, 10); // 传的是指针的地址
// 现在 p 指向了一块堆内存
free(p);
return 0;
}
为什么不能直接传 int *p?因为C语言是值传递。你传进去的是指针的值(地址),函数里修改的是这个值的副本。要想修改指针本身,就得传指针的地址——也就是二级指针。
我的建议:能用一级指针解决的问题,别用二级指针。二级指针容易让人晕。我只有在「需要修改指针指向」或者「处理二维数组」时才用它。平时写代码,能避则避。
4.5 const指针保护:该锁死的就锁死
const关键字用在指针上,有三种组合。很多人搞混,我教你个口诀:「星号左边是所指,星号右边是指针」。
| 写法 | 含义 | 能否修改指向 | 能否修改值 |
|---|---|---|---|
const int *p |
指针指向常量int | 可以 | 不可以 |
int * const p |
指针本身是常量 | 不可以 | 可以 |
const int * const p |
指针和所指都是常量 | 不可以 | 不可以 |
int value = 10;
int another = 20;
const int *p1 = &value;
p1 = &another; // 可以:修改指向
// *p1 = 30; // 错误:不能修改值
int * const p2 = &value;
// p2 = &another; // 错误:不能修改指向
*p2 = 30; // 可以:修改值
const int * const p3 = &value;
// p3 = &another; // 错误
// *p3 = 30; // 错误
我曾经犯过的错:在写一个通信协议栈时,忘了给回调函数的参数加const。结果在回调里不小心修改了接收缓冲区的内容,导致整个数据链路层崩溃。从那以后,我给自己定了个规矩:只要函数不打算修改数据,参数一律加const。这不仅是保护数据,也是在告诉代码的读者:「放心,我不会动你的东西」。
嗯,指针这块内容就讲到这里。说白了,指针就是个工具,用好了事半功倍,用砸了bug满天飞。我的经验是:多写、多调试、多画内存图。每次遇到指针相关的bug,就在纸上把内存布局画出来,地址、值、指向关系一目了然。这招,我用了十几年,屡试不爽。