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;
}

这个声明看起来有点吓人,对吧?我教你一个技巧:从右往左读

  1. get_handler(void) — 这是一个函数,参数为void
  2. int (*const ...)(int) — 返回一个函数指针,指向的函数接受int参数,返回int
  3. const — 这个指针本身是常量

说白了,就是get_handler返回一个你不能修改的函数指针。这种写法在工厂模式或者单例模式中偶尔会用到。

18.6 总结一下

const与函数指针的组合,其实就三种情况:

  • 指针本身const:最常用,用于固定回调表、中断向量表
  • 返回值const:用于只读数据访问
  • 参数const:用于安全回调接口

我个人建议,在实际项目中,优先考虑指针本身const的用法。它最直观,也最能体现const的「只读」语义。至于其他两种,用到的时候再查也不迟。

注意:const函数指针和const成员函数是两码事。C语言没有成员函数的概念,别搞混了。如果你在C++里看到void func() const,那是在说这个成员函数不修改对象状态,和函数指针的const完全不是一回事。

好了,关于const与函数指针,就聊这么多。记住一句话:const在星号右边,修饰指针本身;const在星号左边,修饰指向的数据。搞清楚了这一点,你就能在嵌入式代码里游刃有余地使用const函数指针了。

const修饰函数指针的三种位置 int (*const fp)(int) 指针本身是常量 不能指向其他函数 典型应用 • 中断向量表 • 状态机跳转表 • 固定回调注册 • Bootloader入口表 int const (*fp)(int) 返回值是const 函数返回只读数据 典型应用 • 只读寄存器访问 • 常量数据查询 • 版本信息获取 还有参数const: int (*fp)(int const, int const) 用于安全回调接口 口诀:const在星号右边 → 修饰指针本身;const在星号左边 → 修饰指向的数据

公众号:蓝海资料掘金营,微信deep3321