指针进阶: 回调函数

(1.)回调函数

一、回调函数的定义与核心思想

1. 定义

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给

另一个函数,当这个指针被用来调用其所指向的函数是,我们就说这是回调函数。回调函数不是由

该函数的实现方式直接调用,而是在特定的事件或条件发生时由另外的一方调用,用于对该事件或

条件进行响应

回调函数是通过函数指针调用的函数:将函数的地址(函数指针)作为参数传递给另一个函数,当满足特定条件 / 事件时,被调用的函数通过该指针执行目标函数(回调函数)。

回调函数的核心特征:

  • 不由函数的实现者直接调用,而是由 "外部调用者" 触发;
  • 实现解耦:调用方无需关心回调函数的具体逻辑,只需约定函数指针的接口。
2. 核心基础:函数指针

回调函数依赖 C 语言的函数指针(指向函数的指针,存储函数在内存中的入口地址),需先掌握函数指针的声明、赋值和调用。

(1)函数指针的声明格式
cs 复制代码
返回值类型 (*指针变量名)(参数列表);
  • 括号不可省略:(*fp) 表明fp是指针,而非返回值为指针的函数;
  • 参数列表需与目标函数完全一致(类型、个数、顺序)。
(2)函数指针的赋值与调用
cs 复制代码
// 普通函数
int add(int a, int b) { return a + b; }

int main() {
    // 声明函数指针,匹配add的参数和返回值
    int (*fp)(int, int);
    
    // 赋值:函数名本身就是函数的地址(可省略&)
    fp = add;  // 等价于 fp = &add;
    
    // 调用:两种方式(推荐第一种,更直观)
    int res1 = (*fp)(10, 5);  // 解引用调用
    int res2 = fp(10, 5);     // 简化调用(编译器自动解引用)
    
    printf("res1=%d, res2=%d\n", res1, res2); // 输出:15,15
    return 0;
}

二、回调函数的基本使用步骤

  1. 定义回调函数:符合函数指针的接口约定;
  2. 定义 "注册函数":接收函数指针作为参数,负责触发回调;
  3. 调用注册函数:传递回调函数的地址,触发回调执行。
示例 1:最简回调(无参数无返回值)
cs 复制代码
#include <stdio.h>

// 步骤1:定义回调函数
void myCallback() {
    printf("回调函数被触发!\n");
}

// 步骤2:定义注册函数(接收函数指针参数)
void triggerCallback(void (*callback)()) {
    // 安全检查:避免空指针调用
    if (callback == NULL) {
        printf("回调函数指针为空!\n");
        return;
    }
    // 触发回调
    callback();
}

// 步骤3:调用注册函数,传递回调函数地址
int main() {
    triggerCallback(myCallback); // 输出:回调函数被触发!
    triggerCallback(NULL);       // 输出:回调函数指针为空!
    return 0;
}
示例 2:带参数的回调(业务场景)

实现一个通用的 "计算器",通过回调支持加减乘除,无需修改计算器核心逻辑:

cs 复制代码
#include <stdio.h>

// 步骤1:定义多个回调函数(符合同一函数指针接口)
int add(int a, int b)  { return a + b; }
int sub(int a, int b)  { return a - b; }
int mul(int a, int b)  { return a * b; }
int div(int a, int b)  { return b != 0 ? a / b : 0; }

// 步骤2:注册函数(通用计算器逻辑)
int calculate(int a, int b, int (*calcFunc)(int, int)) {
    if (calcFunc == NULL) return -1;
    return calcFunc(a, b);
}

// 步骤3:调用注册函数,传递不同回调
int main() {
    int a = 20, b = 5;
    printf("加法:%d\n", calculate(a, b, add));  // 25
    printf("减法:%d\n", calculate(a, b, sub));  // 15
    printf("乘法:%d\n", calculate(a, b, mul));  // 100
    printf("除法:%d\n", calculate(a, b, div));  // 4
    return 0;
}

三、标准库中的经典回调:qsort

C 标准库的qsort(快速排序)是回调函数的典型应用,通过回调实现通用排序(支持任意数据类型)。

qsort函数原型
cs 复制代码
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));
  • base:待排序数组的首地址;
  • nitems:数组元素个数;
  • size:单个元素的字节大小;
  • compar:回调函数(比较两个元素,返回值规则:
    • 负数:*a < *b
    • 0:*a == *b
    • 正数:*a > *b)。
示例:用qsort排序整数数组
cs 复制代码
#include <stdio.h>
#include <stdlib.h>

// 回调函数:比较两个整数
int compareInt(const void *a, const void *b) {
    // void* 转换为具体类型(int*),再取值比较
    return *(int *)a - *(int *)b;
}

int main() {
    int arr[] = {9, 3, 7, 1, 8, 5};
    int len = sizeof(arr) / sizeof(arr[0]);
    
    // 调用qsort,传递回调函数
    qsort(arr, len, sizeof(int), compareInt);
    
    // 打印排序结果
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]); // 输出:1 3 5 7 8 9
    }
    return 0;
}
扩展:排序自定义结构体
cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 自定义结构体
typedef struct {
    char name[20];
    int age;
} Person;

// 回调函数:按年龄升序排序
int comparePerson(const void *a, const void *b) {
    return ((Person *)a)->age - ((Person *)b)->age;
}

int main() {
    Person people[] = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}};
    int len = sizeof(people) / sizeof(people[0]);
    
    qsort(people, len, sizeof(Person), comparePerson);
    
    // 打印结果
    for (int i = 0; i < len; i++) {
        printf("%s: %d\n", people[i].name, people[i].age);
    }
    return 0;
}

四、回调函数的典型应用场景

  1. 通用算法 :如qsortbsearch(二分查找),通过回调适配不同数据类型;
  2. 事件驱动编程:如按键触发、网络事件、定时器,回调函数响应事件;
  3. 模块化解耦:底层模块(如驱动、框架)通过回调通知上层业务逻辑,无需耦合上层代码;
  4. 异步操作:如异步 IO、多线程,回调函数处理异步任务的结果;
  5. 钩子函数(Hook):在框架的关键节点插入自定义逻辑(如日志、性能统计)。

五、使用回调函数的注意事项

1. 函数指针类型严格匹配

回调函数的返回值、参数类型 / 个数 / 顺序必须与函数指针声明完全一致,否则会导致:

  • 编译报错(类型不兼容);
  • 运行时未定义行为(如栈溢出、段错误)。
2. 避免空指针调用

调用回调前必须检查函数指针是否为NULL,否则会触发段错误(核心转储):

cs 复制代码
void triggerCallback(void (*callback)()) {
    if (callback != NULL) { // 关键检查
        callback();
    }
}
3. 回调函数的可重入性(线程安全)

若回调函数在多线程中使用:

  • 避免访问全局 / 静态变量(或加互斥锁);
  • 不调用非可重入函数(如strtokprintf需谨慎)。
4. void*的灵活使用

void*是 C 语言实现 "通用回调" 的核心:

  • 注册函数通过void*接收任意类型参数;
  • 回调函数内部将void*转换为具体类型(需保证类型匹配)。
5. 回调函数的生命周期

C 语言中函数地址是全局的(静态存储区),无需担心回调函数 "失效";但如果回调函数是动态生成的(如动态库卸载后),需避免悬空指针。

六、总结

回调函数是 C 语言中实现动态行为、模块解耦、通用算法的核心手段,其本质是函数指针的灵活应用。关键要点:

  1. 函数指针是回调的基础,需严格匹配接口;
  2. 回调的核心价值是 "约定接口,灵活实现";
  3. 实际开发中需注意空指针、线程安全、类型转换等问题。

通过回调函数,可写出高扩展性、低耦合的 C 代码,尤其适合框架开发、驱动开发、通用工具类场景。


(2.)实践代码解析

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ===================== 核心概念说明 =====================
// 回调函数:通过函数指针调用的函数,将函数地址作为参数传递给另一个函数时,被指向的函数即为回调函数
// void* 通用指针特性:可指向任意类型数据,但不能直接解引用/加减运算,需强制转换为具体类型后使用
// 示例:void* p = &num; // 合法;*p = 10; // 非法;((int*)p) = 10; // 合法

// ===================== 通用交换函数(适配任意类型) =====================
// 功能:逐字节交换两个任意类型的数据(核心:char*按字节操作,width指定元素大小)
// 参数:
//   buf1/buf2:待交换数据的起始地址(char*保证按字节访问)
//   width:单个元素的字节大小(如int为4,struct Stu为24)
void Swap(char* buf1, char* buf2, int width) {
    for (int i = 0; i < width; i++) {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;  // 移动到下一字节
        buf2++;
    }
}

// ===================== 比较函数(回调函数,定义排序规则) =====================
// 1. 比较两个整数(适配qsort/自定义冒泡排序)
// 返回值:>0则e1>e2(升序);<0则e1<e2;=0则相等
int cmp_int(const void* e1, const void* e2) {
    return *(int*)e1 - *(int*)e2; // 升序;交换e1/e2则为降序
}

// 2. 学生结构体定义(姓名+年龄)
typedef struct {
    char name[20];  // 字符串(固定长度)
    int age;        // 整数
} Stu;

// 3. 按学生姓名排序(字符串比较,依赖strcmp)
int cmp_stu_by_name(const void* e1, const void* e2) {
    // 转换为Stu*后,取name成员,用strcmp比较(strcmp返回值符合qsort回调规则)
    return strcmp(((Stu*)e1)->name, ((Stu*)e2)->name);
}

// 4. 按学生年龄排序(整数比较)
int cmp_stu_by_age(const void* e1, const void* e2) {
    return ((Stu*)e1)->age - ((Stu*)e2)->age;
}

// ===================== 自定义通用冒泡排序(核心:回调函数解耦排序规则) =====================
// 功能:支持任意类型数据的冒泡排序,排序规则由回调函数cmp决定
// 参数:
//   base:待排序数据的起始地址(void*适配任意类型)
//   sz:数据元素个数
//   width:单个元素的字节大小
//   cmp:比较函数指针(回调函数,定义"谁大谁小")
void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2)) {
    // 外层循环:控制排序轮数(n个元素需n-1轮)
    for (int i = 0; i < sz - 1; i++) {
        int flag = 1; // 优化标记:1表示本轮无交换(已有序),可提前退出
        // 内层循环:每轮比较相邻元素,已排序的末尾i个元素无需比较
        for (int j = 0; j < sz - i - 1; j++) {
            // 核心:计算相邻元素的地址(char*按字节偏移,j*width定位第j个元素)
            // base强制转为char*:保证+width时按字节偏移(void*不能直接加减)
            if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {
                // 前一个元素 > 后一个元素,交换(升序)
                Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
                flag = 0; // 标记本轮有交换
            }
        }
        if (flag == 1) {
            break; // 无交换,说明已有序,提前退出
        }
    }
}

// ===================== 测试函数(验证排序功能) =====================
// 测试1:qsort排序int数组
void test1() {
    int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
    int sz = sizeof(arr) / sizeof(arr[0]);
    printf("===== 测试1:qsort排序int数组 =====\n");
    printf("排序前:");
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    // qsort参数:数组地址、元素个数、单个元素大小、比较回调函数
    qsort(arr, sz, sizeof(int), cmp_int);
    printf("\n排序后:");
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n\n");
}

// 测试2:qsort排序Stu数组(按姓名)
void test2() {
    Stu students[] = {{"ZhangSan", 20}, {"LiSi", 18}, {"WangWu", 22}};
    int sz = sizeof(students) / sizeof(students[0]);
    printf("===== 测试2:qsort排序Stu数组(按姓名) =====\n");
    printf("排序前:\n");
    for (int i = 0; i < sz; i++) {
        printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
    }
    qsort(students, sz, sizeof(Stu), cmp_stu_by_name);
    printf("排序后:\n");
    for (int i = 0; i < sz; i++) {
        printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
    }
    printf("\n");
}

// 测试3:自定义bubble_sort排序int数组
void test3() {
    int arr[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
    int sz = sizeof(arr) / sizeof(arr[0]);
    printf("===== 测试3:自定义bubble_sort排序int数组 =====\n");
    printf("排序前:");
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    bubble_sort(arr, sz, sizeof(int), cmp_int);
    printf("\n排序后:");
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n\n");
}

// 测试4:自定义bubble_sort排序Stu数组(按年龄)
void test4() {
    Stu students[] = {{"ZhaoLiu", 25}, {"QianQi", 21}, {"SunBa", 19}};
    int sz = sizeof(students) / sizeof(students[0]);
    printf("===== 测试4:自定义bubble_sort排序Stu数组(按年龄) =====\n");
    printf("排序前:\n");
    for (int i = 0; i < sz; i++) {
        printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
    }
    bubble_sort(students, sz, sizeof(Stu), cmp_stu_by_age);
    printf("排序后:\n");
    for (int i = 0; i < sz; i++) {
        printf("姓名:%s,年龄:%d\n", students[i].name, students[i].age);
    }
}

// ===================== 主函数(调用测试) =====================
int main() {
    test1(); // qsort排序int
    test2(); // qsort排序Stu(按姓名)
    test3(); // 自定义冒泡排序int
    test4(); // 自定义冒泡排序Stu(按年龄)
    return 0;
}

一、代码详细分析(按模块拆解)

1. 核心设计思想

代码的核心是通过回调函数解耦 "排序逻辑" 和 "数据类型 / 排序规则"

  • 排序核心逻辑(冒泡排序 /qsort)只负责 "比较 - 交换" 的框架,不关心具体数据类型;
  • 具体的 "比较规则" 由回调函数(cmp_int/cmp_stu_by_name等)定义,实现 "一套排序逻辑适配所有数据"。
2. 关键模块解析
(1)void* 通用指针(实现跨类型适配)
  • 为什么用void*?C 语言没有泛型,void*可以指向任意类型数据(int、结构体等),是实现 "通用函数" 的核心。
  • 注意事项:void*不能直接解引用(*p)或加减运算(p+1),必须强制转换为具体类型(如char*/int*/Stu*)后操作。
(2)通用 Swap 函数(按字节交换)
  • 设计思路:无论数据类型是 int(4 字节)还是结构体(24 字节),都可以按字节逐个交换;
  • 核心:char*指针按 1 字节偏移,width参数指定单个元素的总字节数,保证交换完整。
(3)回调函数(比较函数)

所有比较函数都遵循统一的函数指针接口int (*)(const void* e1, const void* e2),这是回调的关键(排序函数只认接口,不认具体实现):

比较函数 功能 实现细节
cmp_int 比较整数 转换为int*后直接相减
cmp_stu_by_name 比较学生姓名 转换为Stu*后调用strcmp
cmp_stu_by_age 比较学生年龄 转换为Stu*后取 age 成员相减
(4)自定义 bubble_sort 函数(回调的核心应用)
  • base参数:void*类型,接收任意数组的首地址;
  • 地址计算:(char*)base + j * width强制转为char*后,j * width能精准定位第 j 个元素(如 int 数组,j=1 时偏移 4 字节);
  • 回调调用:cmp(...) > 0时交换元素,排序规则完全由cmp决定(换个回调函数就能改排序规则,无需改 bubble_sort 逻辑);
  • 优化:flag标记无交换时提前退出,减少不必要的循环。
(5)标准库 qsort 函数(经典回调应用)

qsort的设计思路和自定义 bubble_sort 完全一致,只是排序算法是快速排序:

  • 函数原型:void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *))
  • 调用方式:只需传递 "数组地址 + 元素个数 + 元素大小 + 比较回调",即可完成任意类型排序。
3. 运行结果(验证功能)

编译运行代码后,输出如下(按测试函数顺序):

cs 复制代码
===== 测试1:qsort排序int数组 =====
排序前:3 1 4 1 5 9 2 6 
排序后:1 1 2 3 4 5 6 9 

===== 测试2:qsort排序Stu数组(按姓名) =====
排序前:
姓名:ZhangSan,年龄:20
姓名:LiSi,年龄:18
姓名:WangWu,年龄:22
排序后:
姓名:LiSi,年龄:18
姓名:WangWu,年龄:22
姓名:ZhangSan,年龄:20

===== 测试3:自定义bubble_sort排序int数组 =====
排序前:9 8 7 6 5 4 3 2 1 
排序后:1 2 3 4 5 6 7 8 9 

===== 测试4:自定义bubble_sort排序Stu数组(按年龄) =====
排序前:
姓名:ZhaoLiu,年龄:25
姓名:QianQi,年龄:21
姓名:SunBa,年龄:19
排序后:
姓名:SunBa,年龄:19
姓名:QianQi,年龄:21
姓名:ZhaoLiu,年龄:25
4. 扩展与优化点
  • 降序排序:只需修改比较函数(如cmp_int改为*(int*)e2 - *(int*)e1);
  • 线程安全:若在多线程中使用,需保证回调函数无全局 / 静态变量(或加锁);
  • 性能优化:自定义 bubble_sort 可替换为快速排序 / 归并排序,只需保留回调接口,核心逻辑不变。

二、总结

该代码完整展示了 C 语言回调函数的核心价值:接口与实现分离。通过函数指针(回调),排序函数无需关心 "排什么数据、按什么规则排",只需遵守统一的比较接口;而具体的比较规则可灵活定制,实现了极高的扩展性。这也是 Linux 内核、标准库等底层代码中回调函数的核心应用模式。

相关推荐
Azxcc02 小时前
c++ core guidelines解析--让接口易于使用
开发语言·c++
helloworddm2 小时前
NSIS编写C/C++扩展
c语言·开发语言·c++
Vanranrr2 小时前
一个由非虚函数导致的隐藏Bug:窗口显示异常问题排查与解决
开发语言·bug
前端小臻2 小时前
react中的函数组件和类组件(快捷指令和区别)
前端·react.js·前端框架
烤麻辣烫2 小时前
黑马大事件学习-15(前端登录页面)
前端·css·vue.js·学习·html
Cache技术分享2 小时前
266. Java 集合 - ArrayList vs LinkedList 内存使用深度剖析
前端·后端
曹牧2 小时前
Java:Jackson库序列化对象
java·开发语言·python
豆苗学前端2 小时前
彻底讲透浏览器的事件循环,吊打面试官
前端·javascript·面试
来自上海的这位朋友2 小时前
从零打造一个无依赖的Canvas图片编辑器
javascript·vue.js·canvas