【C语法硬核20讲】08 函数指针:回调与状态机

目标:掌握 函数指针(function pointer) 的声明/赋值/调用、**回调(callback)**设计模式、表驱动(table-driven)与有限状态机(finite state machine, FSM) 的实战写法,以及类型不匹配与上下文传递的避坑。


1)基础语法速记

int add(int,int);
int (*fp)(int,int) = add;   // 声明并赋值
int r = fp(3,4);            // 调用

建议用 typedef 降低心智负担

typedef int (*binop_t)(int,int);
int sub(int a,int b){ return a-b; }
binop_t op = sub;

2)标准库里的回调案例:qsort

int cmp_int(const void *a,const void *b){
    int x = *(const int*)a, y = *(const int*)b;
    return (x>y) - (x<y);
}
qsort(arr, n, sizeof arr[0], cmp_int);

签名必须完全匹配。强转错误签名是 未定义行为。


3)回调 + 上下文(context)传递模式

很多 API 只给你一个 void* 存上下文。

typedef void (*on_event_t)(void *ctx, int code);

struct Handler {
    void       *ctx;
    on_event_t  on_event;
};

void dispatch(struct Handler *h, int code){
    if (h->on_event) h->on_event(h->ctx, code);
}

好处:回调可无状态(把状态放 ctx),也可复用同一个处理函数配不同上下文。


4)表驱动:用函数指针数组消灭 switch-case

typedef int (*uop_t)(int);

static int op_inc(int x){ return x+1; }
static int op_dec(int x){ return x-1; }
static int op_neg(int x){ return -x; }

static uop_t OPS[] = { op_inc, op_dec, op_neg };

int apply(int which, int x){
    if (which < 0 || which >= (int)(sizeof OPS/sizeof OPS[0])) return x;
    return OPS[which](x);
}

扩展性:新增操作只需新增一个函数并放进表。


5)有限状态机(FSM)两种写法

A.表驱动 FSM(状态 × 事件 → 处理器)

typedef enum { S_INIT, S_OPEN, S_CLOSED, S_N } state_t;
typedef enum { E_OPEN, E_CLOSE, E_TIMEOUT, E_N } event_t;

typedef state_t (*handler_t)(void *ctx);

struct Cell { handler_t h; };

state_t on_init_open(void*), on_open_close(void*), on_any_timeout(void*);

static struct Cell T[S_N][E_N] = {
/*           OPEN            CLOSE              TIMEOUT */
 [S_INIT]  ={ on_init_open,  NULL,              on_any_timeout },
 [S_OPEN]  ={ NULL,          on_open_close,     on_any_timeout },
 [S_CLOSED]={ NULL,          NULL,              on_any_timeout },
};

state_t dispatch(void *ctx, state_t s, event_t e){
    handler_t h = T[s][e].h;
    return h ? h(ctx) : s;
}

优点:结构清晰,易于审计与单测。

B.状态函数式(状态函数返回下一个状态)

typedef state_t (*state_fn)(void *ctx, event_t e);

state_t st_init(void *ctx, event_t e){
    switch(e){ case E_OPEN: /* ... */ return S_OPEN; default: return S_INIT; }
}

变体:用 state_fn 数组 state_fn STATES[S_N],每个状态有一个处理函数。


6)回调注册 API 的设计模板

typedef void (*cb_t)(void *ctx, int evt, const void *data);

struct Bus {
    cb_t    cb;
    void   *ctx;
};

void bus_subscribe(struct Bus *b, cb_t cb, void *ctx){
    b->cb = cb; b->ctx = ctx;
}
void bus_emit(struct Bus *b, int evt, const void *data){
    if (b->cb) b->cb(b->ctx, evt, data);
}

注意:API 里始终把上下文放第一参数,形成统一风格。


7)避坑与性能

  • 签名必须匹配(参数与返回值类型);强转只是抹掉类型检查,不能修正不匹配
  • 不要把带 ... 可变参数的函数指针混用到固定参数签名。
  • 热路径频繁小回调:可改为函数指针表 + static inline 包装,或直接开关宏避免间接跳转开销。
  • 嵌入式/ISR:有的平台对函数指针调用有额外开销或限制,需基准测试。
  • 与线程:回调里修改共享状态需加锁或用 _Atomic。

8)自检清单

  • 会写 typedef 简化声明
  • 会传 void* ctx 做上下文
  • 会用函数指针数组实现表驱动
  • FSM 能用“表”或“状态函数”两种模式落地
  • 知道签名不匹配是 UB,绝不强转“糊弄”

9)迷你练习

1)把 qsort 的比较器改为“降序”。
2)为一个简易解析器设计状态表:S_NUM/S_OP/S_ERR × 事件 E_DIGIT/E_PLUS/E_OTHER。
3)把日志后端做成回调:可注册“写到文件”或“写到 socket”。


小结

  • static/extern:管可见性与生命期
  • 宏:用全括号、do-while(0)、无副作用,该用 static inline 就用;
  • 函数指针:用在回调、表驱动、状态机,签名要一丝不苟,上下文用 void* 传递。
原文链接:,转发请注明来源!