18、const与函数指针:const修饰函数指针的含义与用法
说实话,const和函数指针放在一起,很多C语言开发者都会皱眉头。我见过不少写了三五年嵌入式代码的朋友,一看到const出现在函数指针声明里,就开始挠头。嗯,这确实是个容易混淆的点,但搞清楚了,你会发现它其实很实用。
18.1 先搞清楚:const到底修饰谁?
我们先从最基础的开始。const修饰函数指针,关键要看它放在哪里。我习惯把函数指针的声明拆开来看,这样不容易晕。
先看一个普通的函数指针:
// 定义一个函数指针类型
typedef int (*FuncPtr)(int, int);
// 或者直接声明一个函数指针变量
int (*fp)(int, int);
现在,我们把const加进去。const可以出现在三个不同的位置,含义完全不同:
| 写法 | 含义 | 实际场景 |
|---|---|---|
int (*const fp)(int, int) |
指针本身是常量(不能指向别的函数) | 回调表、固定跳转表 |
int const (*fp)(int, int) |
返回值是常量(函数返回const int) | 只读数据访问函数 |
int (*fp)(int const, int const) |
参数是常量(函数内部不能修改参数) | 安全回调接口 |
你看,同一个const,位置不同,意思天差地别。我在项目中就见过有人把const放错位置,结果编译报错找了半天。
18.2 最常见的用法:const修饰指针本身
实际工作中,用得最多的是第一种——int (*const fp)(int, int)。说白了,就是让这个函数指针变量本身变成只读的,不能再指向别的函数。
为什么要这么做?我举个例子。你在写一个Bootloader,里面有一张中断向量表:
// 定义中断处理函数类型
typedef void (*ISR_Handler)(void);
// 中断向量表 - 指针本身是常量
ISR_Handler const vector_table[32] = {
&reset_handler,
&nmi_handler,
&hard_fault_handler,
// ... 其他中断入口
};
这里const修饰的是数组里的每个指针元素。也就是说,这些指针一旦初始化,就不能再被修改。这很重要——你总不希望运行时有人把复位向量给改了吧?
核心要点:const放在*的右边,修饰的是指针变量本身。放在*的左边,修饰的是指针指向的数据。
18.3 避坑指南:const函数指针作为参数
我曾经在一个通信协议栈里踩过一个坑。当时我需要把一个回调函数注册到驱动层,驱动层要求这个回调不能被修改。我一开始是这么写的:
// 驱动层的注册接口
void register_callback(int (*const cb)(int));
// 我的实现
int my_handler(int data) {
return data * 2;
}
// 注册
register_callback(my_handler);
嗯,这样写没问题。但问题出在——如果驱动层想保存这个回调指针,它得复制一份。而const只保证传入的指针本身不能被修改,不保证驱动层内部保存的副本也是const的。你想想看,这就有漏洞了。
正确的做法是:驱动层内部也要用const来声明保存的指针:
// 驱动层内部
static int (*const saved_cb)(int) = NULL; // 保存的回调也是const
void register_callback(int (*const cb)(int)) {
// saved_cb = cb; // 如果saved_cb是const,这里会报错
// 所以实际上,驱动层不能直接保存const指针
// 需要重新设计接口
}
你看,这里就出现了一个矛盾。const函数指针作为参数传入后,接收方如果想保存它,要么放弃const,要么用memcpy。我个人建议:如果回调函数指针需要被保存,就不要在参数上加const,而是在保存的地方加。
18.4 实战:const回调表驱动状态机
我去年做的一个物联网网关项目里,用到了const修饰的函数指针表来驱动状态机。效果非常好,分享给你看看:
// 状态处理函数类型
typedef int (*StateHandler)(void *context);
// 状态表 - 每个状态对应一个处理函数
// 注意:const修饰指针本身,确保状态表不可篡改
static StateHandler const state_table[] = {
[STATE_INIT] = &state_init,
[STATE_CONNECT] = &state_connect,
[STATE_RUNNING] = &state_running,
[STATE_ERROR] = &state_error,
[STATE_SLEEP] = &state_sleep,
};
// 状态机执行函数
void run_state_machine(int current_state, void *ctx) {
if (current_state < 0 || current_state >= MAX_STATES) {
return;
}
// 通过const指针调用
StateHandler handler = state_table[current_state];
if (handler) {
int result = handler(ctx);
// 处理结果...
}
}
这里state_table里的每个函数指针都是const的。好处是什么?
- 安全性:状态表一旦初始化,运行时不会被意外修改
- 可读性:看到const,别人就知道这张表是固定的
- 优化机会:编译器可以把整个表放到只读区(如Flash),节省RAM
小技巧:在嵌入式系统中,把const函数指针表放到Flash里,可以显著减少RAM占用。我习惯在声明时加上__attribute__((section(".rodata")))来显式控制存放位置。
18.5 const与函数指针返回值的微妙之处
还有一种情况:函数返回一个const函数指针。比如:
// 返回一个const函数指针
int (*const get_handler(void))(int) {
static int (*const handler)(int) = &default_handler;
return handler;
}
这个声明看起来有点吓人,对吧?我教你一个技巧:从右往左读。
get_handler(void)— 这是一个函数,参数为voidint (*const ...)(int)— 返回一个函数指针,指向的函数接受int参数,返回intconst— 这个指针本身是常量
说白了,就是get_handler返回一个你不能修改的函数指针。这种写法在工厂模式或者单例模式中偶尔会用到。
18.6 总结一下
const与函数指针的组合,其实就三种情况:
- 指针本身const:最常用,用于固定回调表、中断向量表
- 返回值const:用于只读数据访问
- 参数const:用于安全回调接口
我个人建议,在实际项目中,优先考虑指针本身const的用法。它最直观,也最能体现const的「只读」语义。至于其他两种,用到的时候再查也不迟。
注意:const函数指针和const成员函数是两码事。C语言没有成员函数的概念,别搞混了。如果你在C++里看到void func() const,那是在说这个成员函数不修改对象状态,和函数指针的const完全不是一回事。
好了,关于const与函数指针,就聊这么多。记住一句话:const在星号右边,修饰指针本身;const在星号左边,修饰指向的数据。搞清楚了这一点,你就能在嵌入式代码里游刃有余地使用const函数指针了。