在底层开发中,如何写出像Java/C++ 模板那样通用的代码?本篇将带你深入 C 语言的核心——泛型指针与回调函数,手把手教你构建一个高可用的通用排序框架。
完整工程代码预览
文件1:person.h(业务数据定义)
#pragma once// 业务层:定义需要排序的复杂结构体typedef struct { char name[50]; int age;} Person;
文件2:compare.h(核心协议定义)
#pragma once#include<stddef.h>// 【核心协议】定义通用的比较函数指针类型// 参数:两个待比较元素的指针,以及一个用于扩展的上下文 contexttypedefint(*CompareFunc)(constvoid* a, constvoid* b, void* context);// 各种类型的比较器声明intintCompare(constvoid* a, constvoid* b, void* context);intstringCompare(constvoid* a, constvoid* b, void* context);intcompareByName(constvoid* a, constvoid* b, void* context);intcompareByAge(constvoid* a, constvoid* b, void* context);
文件3:sort.h(通用接口声明)
#pragma once#include<stddef.h>#include"compare.h"// 【通用排序接口】// 支持任何类型的数组,通过回调函数实现排序逻辑解耦voidgenericSort(void* array, size_t element_size, size_t element_count, CompareFunc compare, void* context);
文件4:compare.c(比较逻辑实现)
#include"compare.h"#include"person.h"#include<string.h>// 整数比较:演示如何将 void* 还原为原始类型intintCompare(constvoid* a, constvoid* b, void* context){ (void)context; int valA = *(const int*)a; int valB = *(const int*)b; return valA - valB;}// 字符串数组比较:处理二级指针的转换细节intstringCompare(constvoid* a, constvoid* b, void* context){ (void)context; const char* s1 = *(const char**)a; const char* s2 = *(const char**)b; return strcmp(s1, s2);}// 结构体比较:按名字(字符串顺序)intcompareByName(constvoid* a, constvoid* b, void* context){ (void)context; const Person* pA = (const Person*)a; const Person* pB = (const Person*)b; return strcmp(pA->name, pB->name);}// 结构体比较:按年龄(数值顺序)intcompareByAge(constvoid* a, constvoid* b, void* context){ (void)context; const Person* pA = (const Person*)a; const Person* pB = (const Person*)b; return pA->age - pB->age;}
文件5:sort.c(底层适配器实现)
#include"sort.h"#include<stdlib.h>// 由于 C 标准库 qsort 不支持传递 context,我们使用静态全局变量进行“中转”// 这是一个典型的适配器模式应用static CompareFunc currentCompare;static void* currentContext;// 包装器函数:桥接标准 qsort 的签名要求staticintinternalWrapper(constvoid* a, constvoid* b){ return currentCompare(a, b, currentContext);}voidgenericSort(void* array, size_t element_size, size_t element_count, CompareFunc compare, void* context) { // 1. 暂存回调函数和上下文对象 currentCompare = compare; currentContext = context; // 2. 调用标准库 qsort,它是泛型编程的经典实践 qsort(array, element_count, element_size, internalWrapper);}
文件6:main.c(测试与策略应用)
#include<stdio.h>#include"sort.h"#include"person.h"intmain(){ Person group[] = { {"Frank", 30}, {"Alice", 18}, {"Bob", 25}, {"Zack", 40} }; size_t n = sizeof(group) / sizeof(group[0]); // 运行时决定排序策略:0-按名,1-按年龄 CompareFunc strategies[] = { compareByName, compareByAge }; int choice = 1; printf(">>> 正在执行泛型排序...\n"); genericSort(group, sizeof(Person), n, strategies[choice], NULL); for (size_t i = 0; i < n; i++) { printf("结果: %s, %d\n", group[i].name, group[i].age); } return 0;}
核心逻辑深度拆解
在compare.h中,我们定义了参数为const void*的函数指针。
观察main.c中的strategies数组:
CompareFunc strategies[] = { compareByName, compareByAge };
这是一种高级的设计模式——策略模式。
这是本课最精彩的架构细节。C 标准库提供的 qsort 签名是固定的,它不接受context参数。
痛点:如果你在排序时需要依赖某些外部状态(比如多语言排序的字典),标准qsort 做不到。
方案:在sort.c中,我们定义了一个静态包装器internalWrapper。
逻辑:我们将用户的compare函数和context存入静态变量,然后让标准qsort去调包装器,包装器内部再把context喂给用户的函数。这就像一个电源适配器,将不匹配的接口强行连接在一起。
在genericSort调用中,有两个参数至关重要:element_size和element_count。
结语
安全性思考:为什么在比较函数中要使用const void*而不是void*?(提示:保护原始数据不被意外篡改)。
性能思考:回调函数会带来额外的内存开销吗?(提示:函数调用会有压栈开销,但在大型数组排序中,这部分开销相对于O(n log n) 的算法复杂度几乎可以忽略)。
局限性分析:我们在sort.c中使用了静态全局变量,这在多线程环境下会有什么隐患?(顶级开发者会意识到:这会导致竞态条件,需要使用线程局部存储Thread Local Storage或平台相关的qsort_s来解决)。