前面我们写了不少函数排序int的冒泡、交换int的swap、求int数组的最大值……但你有没有发现这些函数都被死死绑在了具体的类型上——排序只能排int交换只能换int如果哪天想排double或struct Student就得把几乎相同的逻辑重新写一遍。这显然不够优雅。C 语言虽然没有模板和泛型但它有一个“万能指针”——void *。配合函数指针你可以写出类型无关的通用代码写一次到处用。一、void *可以指向任何类型的指针void *是一种特殊的指针类型它能存储任意类型对象的地址但不携带“指向什么类型”的信息。inta10;doubleb3.14;charcX;void*vp;vpa;// 指向 intvpb;// 指向 doublevpc;// 指向 char你可以把void *理解为“一个单纯的地址”丢掉了类型标签。正因如此编译器不知道它指向的数据是什么类型、占多少字节所以限制 1不能直接解引用inta10;void*vpa;printf(%d\n,*vp);// 编译错误不能解引用 void*必须先把它强制转换成正确的类型指针再解引用printf(%d\n,*(int*)vp);// 正确限制 2不能做指针算术vp1;// 编译错误不知道每次跳多少字节GCC 扩展允许void*的算术当成char*但标准 C 不允许。要移动void*先转成char*。二、void *的典型用途通用接口尽管有这些限制void *却成为了 C 语言实现“泛型”的基石。标准库中大量函数都使用它函数用途malloc/calloc/realloc返回void*可赋给任何指针memcpy/memset/memmove操作任意类型的内存块qsort/bsearch排序和查找任何类型的数据线程创建函数传递任意类型的参数它们的共同点是操作的是“一段内存”不关心里面存的是什么类型。只要我们告诉它这块内存的起始地址和大小剩下的事情就能做。三、实现一个通用的swap函数以前我们写的swap只能交换两个int。用void*加上元素大小参数就能交换任意类型的值。#includestdio.h#includestring.h// 交换任意两块内存voidswap_generic(void*a,void*b,size_tsize){chartemp[size];// VLA临时存储memcpy(temp,a,size);memcpy(a,b,size);memcpy(b,temp,size);}intmain(void){intx10,y20;swap_generic(x,y,sizeof(int));printf(x%d, y%d\n,x,y);// x20, y10doubled11.5,d23.5;swap_generic(d1,d2,sizeof(double));printf(d1%.1f, d2%.1f\n,d1,d2);// d13.5, d21.5return0;}关键我们必须传入sizeof(类型)因为void*不知道元素多大。函数内部用memcpy也是void*接口逐字节拷贝char temp[size]是变长数组大小由参数决定。变长数组在某些场合有争议栈空间风险但它让这个例子简洁。实际项目中若拷贝大对象可改用动态分配或循环按机器字长拷贝。原理一样。四、模仿qsort写一个泛型冒泡排序qsort是 C 标准库的通用排序函数原型如下voidqsort(void*base,size_tnmemb,size_tsize,int(*compar)(constvoid*,constvoid*));参数base数组首地址void*nmemb元素个数size每个元素的字节数compar比较函数接收两个const void*返回负数/零/正数它的思想是把数据和操作分离。排序逻辑不需要知道数据是什么类型只要知道每个元素多大并由调用者提供一个比较函数。我们参照这个设计实现自己的generic_bubble_sort#includestdio.h#includestring.h#includestddef.h// 交换 size 字节的两块内存staticvoidswap_bytes(void*a,void*b,size_tsize){char*pa(char*)a;char*pb(char*)b;for(size_ti0;isize;i){chartemppa[i];pa[i]pb[i];pb[i]temp;}}// 泛型冒泡排序voidgeneric_bubble_sort(void*base,size_tnmemb,size_tsize,int(*compar)(constvoid*,constvoid*)){char*arr(char*)base;// 转换为 char* 用于地址算术for(size_ti0;inmemb-1;i){intswapped0;for(size_tj0;jnmemb-1-i;j){// 计算相邻元素的地址void*elem_jarrj*size;void*elem_nextarr(j1)*size;if(compar(elem_j,elem_next)0){swap_bytes(elem_j,elem_next,size);swapped1;}}if(!swapped)break;}}核心点base被转成char*这样arr j * size就能准确定位到第j个元素的起始地址。调用者提供的compar负责比较逻辑它拿到的是两个void*内部再转成实际类型比较。五、使用我们的泛型排序对int、double、结构体排序1. 排序int数组intcompare_int(constvoid*a,constvoid*b){intia*(constint*)a;intib*(constint*)b;return(iaib)-(iaib);// 简洁的差值法避免溢出}intmain(void){intarr[]{5,2,9,1,5,6};size_tnsizeof(arr)/sizeof(arr[0]);generic_bubble_sort(arr,n,sizeof(int),compare_int);for(size_ti0;in;i)printf(%d ,arr[i]);printf(\n);return0;}2. 排序double数组intcompare_double(constvoid*a,constvoid*b){doubleda*(constdouble*)a;doubledb*(constdouble*)b;if(dadb)return-1;if(dadb)return1;return0;}注意浮点数不能直接用减法返回因为 NaN 和极值可能使减法失效。3. 排序结构体按成绩typedefstruct{charname[20];intid;floatscore;}Student;intcompare_student_by_score(constvoid*a,constvoid*b){constStudent*sa(constStudent*)a;constStudent*sb(constStudent*)b;if(sa-scoresb-score)return-1;if(sa-scoresb-score)return1;return0;}intmain(void){Student class[]{{Alice,1,92.5},{Bob,2,85.0},{Carol,3,78.5},};size_tnsizeof(class)/sizeof(class[0]);generic_bubble_sort(class,n,sizeof(Student),compare_student_by_score);for(size_ti0;in;i){printf(%s: %.1f\n,class[i].name,class[i].score);}return0;}你看到了吗同一个generic_bubble_sort函数通过传入不同的比较函数可以排序任何类型——这就是 C 语言泛型编程的核心模式void* 元素大小 函数指针。六、void *的更多场景除了排序void*还在以下地方大显身手动态内存管理malloc返回void*你就是用它来申请任意类型的内存。回调参数线程创建、定时器回调等通常接受一个void*参数让你传递任意上下文。通用容器简单的泛型栈、队列、链表可以通过void*存储数据配合大小或用指针指向数据。例如一个泛型栈可以这样声明typedefstruct{void**data;// 存的是各种类型的指针inttop;intcapacity;}Stack;这种“存指针”的方式省去了元素大小的麻烦但需要注意数据生命周期。更完善的通用容器库如 GLib、sys/queue.h正是建立在这些概念上。七、常见错误与陷阱1. 忘记传递size或写错voidbad_sort(void*base,size_tnmemb,int(*cmp)(constvoid*,constvoid*)){// 缺少 size 参数无法知道元素多大}没有size就无法计算第 n 个元素的地址排序逻辑寸步难行。2.compar中强制转换错误intcompare_int(constvoid*a,constvoid*b){return*(int*)a-*(int*)b;// 未加 const且解引用 void* 时忘了 const}应该*(const int*)a。虽然编译器对const丢失可能只是警告但保持 const 正确性是好习惯。3. 对void*做算术void*vp...;vp;// GCC 扩展允许但标准 C 不允许可移植性差统一用(char*)vp offset。4. 比较函数返回值逻辑错误qsort风格的比较函数要求返回负数表示a b零表示相等正数表示a b。简单减法虽然简便但对大整数可能溢出。对浮点数务必用 if-else 结构。八、小结void *是 C 语言实现通用编程的秘密武器。它丢掉了类型标签换来了操作任意数据的能力但也要求程序员必须小心翼翼地管理类型转换和大小信息。今天你学到了void*可以指向任何类型但不能直接解引用或做算术。配合size参数和函数指针可以写出类型无关的swap、排序、查找等算法。自己实现了一个泛型冒泡排序理解了qsort的底层原理。这是 C 语言中“数据和算法分离”的经典范式。这套模式是后面实现链表、树、哈希表等通用数据结构的基础。下一篇文章我们就要用void*和动态内存构建一个经典的动态数据结构——单向链表让数据不在编译时固定大小而是可以自由地增长和收缩。真正的数据结构之旅现在开始。课后小练习实现一个泛型的数组逆置函数void generic_reverse(void *base, size_t nmemb, size_t size)将数组中元素的顺序颠倒。用它逆置一个int数组和一个double数组验证结果。利用qsort标准库函数对字符串数组char *words[] {banana, apple, cherry};按字典序排序并打印。提示比较函数中解引用void*得到char**再用strcmp。实现一个泛型的find_max函数void *find_max(const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *))返回指向最大元素的指针。测试对int数组和struct Student数组的查找。小挑战用void*和函数指针设计一个简单的回调式foreach函数void array_foreach(void *base, size_t nmemb, size_t size, void (*func)(void *))。该函数遍历数组对每个元素调用func。写一个打印int和double的回调并在main中测试。我们下期见获取本系列示例代码请访问 GitCode 仓库。