C 语言 Callback 机制解析:函数指针、上下文参数与工程实践
核心结论:Callback 的本质是把“以后要执行的函数”作为参数传递,使通用流程不必依赖具体行为。
问题背景
普通函数调用会把“流程”和“行为”直接写死:
void worker(int value)
{
int result = value * 2;
printf("result = %d\n", result);
}
worker() 既负责计算,也决定如何处理结果。如果后来需要把结果写入文件、发送到网络或者更新统计信息,就只能修改 worker(),或者为每种行为重新写一套流程。
Callback(回调)解决的是这类耦合问题:worker() 只负责“什么时候调用”,调用者负责提供“具体调用什么”。在 C 语言中,Callback 通过函数指针实现。
函数指针是 Callback 的基础
下面的声明表示:callback 是一个函数指针,它指向“接收一个 int 参数、没有返回值”的函数。
void (*callback)(int);
可以从变量名向外阅读:
*callback:callback是一个指针。(*callback)(int):它指向的对象可以像函数一样接收一个int。- 最前面的
void:该函数没有返回值。
括号不能省略。下面的声明不是函数指针变量,而是一个函数声明:
void *callback(int);
它表示 callback 是函数,接收一个 int,返回 void *。
为了避免复杂声明反复出现,工程代码通常使用 typedef:
typedef void (*result_callback_t)(int result);
此后,result_callback_t 就代表一种函数类型约束。能够作为该 Callback 传入的函数,参数和返回值必须与它兼容。
Callback 的传递与调用
一个完整的同步回调示例如下:
#include <stdio.h>
typedef void (*result_callback_t)(int result);
void print_result(int result)
{
printf("result = %d\n", result);
}
void worker(int value, result_callback_t callback)
{
int result = value * 2;
if (callback != NULL) {
callback(result);
}
}
int main(void)
{
worker(10, print_result);
return 0;
}
输出结果为:
result = 20
这段代码是如何工作的?
print_result出现在表达式中时,会转换为指向该函数的指针。main()把这个地址传给worker()。worker()完成计算,但不关心结果最终如何处理。callback(result)通过函数地址间接调用print_result()。
也可以写成 (*callback)(result)。两种写法语义相同,前一种更常见。
用 Callback 把算法和操作解耦
Callback 不只是“计算结束后通知一下”,还可以把一段可替换的操作注入通用算法。
#include <stdio.h>
typedef int (*binary_operation_t)(int left, int right);
int add(int left, int right)
{
return left + right;
}
int multiply(int left, int right)
{
return left * right;
}
int calculate(int left, int right, binary_operation_t operation)
{
if (operation == NULL) {
return 0;
}
return operation(left, right);
}
int main(void)
{
printf("add: %d\n", calculate(10, 3, add));
printf("multiply: %d\n", calculate(10, 3, multiply));
return 0;
}
输出结果为:
add: 13
multiply: 30
calculate() 是稳定的控制流程,add() 和 multiply() 是可以替换的策略。新增除法时,不需要修改 calculate(),只需增加一个签名兼容的函数。
这也是 Callback 最重要的工程价值:依赖关系从“通用模块调用具体模块”反转为“具体模块把行为注册给通用模块”。
使用 void *context 传递调用上下文
只有函数地址通常不够。回调函数还可能需要访问对象状态、配置或统计数据。C 没有闭包,常见做法是同时传递一个 void *context。
#include <stdio.h>
typedef void (*event_callback_t)(int event, void *context);
typedef struct {
const char *name;
int event_count;
} event_stats_t;
void record_event(int event, void *context)
{
event_stats_t *stats = context;
stats->event_count++;
printf("%s received event %d, total = %d\n",
stats->name, event, stats->event_count);
}
void publish_event(int event,
event_callback_t callback,
void *context)
{
if (callback != NULL) {
callback(event, context);
}
}
int main(void)
{
event_stats_t stats = {
.name = "storage-worker",
.event_count = 0,
};
publish_event(1001, record_event, &stats);
publish_event(1002, record_event, &stats);
return 0;
}
函数指针描述“执行哪段代码”,context 描述“针对哪份数据执行”。两者组合后,可以模拟面向对象语言中的“对象方法”。
⚠️ 同步回调可以安全使用仍在当前调用栈中的局部变量。异步回调可能在注册函数返回后才执行,此时不能继续保存已经失效的栈地址,必须明确上下文的生命周期和释放责任。
标准库和线程接口中的 Callback
qsort() 的比较函数
标准库 qsort() 不知道元素应该按什么规则排序,因此要求调用者传入比较函数:
#include <stdio.h>
#include <stdlib.h>
int compare_int(const void *left, const void *right)
{
int a = *(const int *)left;
int b = *(const int *)right;
return (a > b) - (a < b);
}
int main(void)
{
int values[] = {4, 1, 3, 2};
size_t count = sizeof(values) / sizeof(values[0]);
qsort(values, count, sizeof(values[0]), compare_int);
for (size_t i = 0; i < count; ++i) {
printf("%d ", values[i]);
}
printf("\n");
return 0;
}
qsort() 管理排序流程,compare_int() 定义元素顺序。这是典型的 Callback 设计。
pthread_create() 的线程入口
pthread_create() 也接收一个函数地址:
#include <pthread.h>
#include <stdio.h>
void *thread_entry(void *argument)
{
int value = *(int *)argument;
printf("worker: %d\n", value);
return NULL;
}
int main(void)
{
pthread_t thread;
int value = 10;
pthread_create(&thread, NULL, thread_entry, &value);
pthread_join(thread, NULL);
return 0;
}
线程入口在形式上属于 Callback:调用者提供函数,线程库在合适的时机调用它。但产生并发的是 pthread_create(),不是 Callback 这种语法本身。
Callback 最容易产生的误解
Callback 是否一定是异步的?
不是。前面的 worker()、calculate() 和 qsort() 都会在当前线程中同步调用 Callback。只有当框架把函数交给线程、事件循环或异步 I/O 系统时,回调才可能异步执行。
注册 Callback 是否等于执行 Callback?
不是。注册只是保存函数地址。真正执行发生在框架调用该函数指针时。有些 Callback 会立即执行,有些可能永远不会被触发。
函数签名不一致时能否强制转换?
不应这样做。把不兼容的函数指针强制转换后再调用,会产生未定义行为。参数数量、参数类型和返回值都应符合接口定义。
Callback 的上下文由谁释放?
C 语言没有自动所有权规则,接口必须约定:上下文由调用者保持有效,还是由回调系统接管并释放。异步系统中的大量悬空指针问题,都来自这项约定不清晰。
总结
Callback 不是一种并发机制,而是一种控制反转手段:函数指针决定执行行为,void *context 携带调用状态,二者共同实现通用流程与具体策略的解耦。