C 语言 Callback 机制解析:函数指针、上下文参数与工程实践

作者:CherryYang 发布时间: 2026-08-12 阅读量:11 评论数:0

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

可以从变量名向外阅读:

  1. *callbackcallback 是一个指针。
  2. (*callback)(int):它指向的对象可以像函数一样接收一个 int
  3. 最前面的 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

这段代码是如何工作的?

  1. print_result 出现在表达式中时,会转换为指向该函数的指针。
  2. main() 把这个地址传给 worker()
  3. worker() 完成计算,但不关心结果最终如何处理。
  4. 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 携带调用状态,二者共同实现通用流程与具体策略的解耦。

评论