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,就在纸上把内存布局画出来,地址、值、指向关系一目了然。这招,我用了十几年,屡试不爽。