当前位置: 首页 > news >正文

28. 【C语言】通用数据操作:`void *` 与类型无关编程

前面我们写了不少函数:排序int的冒泡、交换intswap、求int数组的最大值……但你有没有发现,这些函数都被死死绑在了具体的类型上——排序只能排int,交换只能换int,如果哪天想排doublestruct Student,就得把几乎相同的逻辑重新写一遍。

这显然不够优雅。C 语言虽然没有模板和泛型,但它有一个“万能指针”——void *。配合函数指针,你可以写出类型无关的通用代码,写一次,到处用。


一、void *:可以指向任何类型的指针

void *是一种特殊的指针类型,它能存储任意类型对象的地址,但不携带“指向什么类型”的信息。

inta=10;doubleb=3.14;charc='X';void*vp;vp=&a;// 指向 intvp=&b;// 指向 doublevp=&c;// 指向 char

你可以把void *理解为“一个单纯的地址”,丢掉了类型标签。正因如此,编译器不知道它指向的数据是什么类型、占多少字节,所以:

限制 1:不能直接解引用

inta=10;void*vp=&a;printf("%d\n",*vp);// 编译错误!不能解引用 void*

必须先把它强制转换成正确的类型指针,再解引用:

printf("%d\n",*(int*)vp);// 正确

限制 2:不能做指针算术

vp+1;// 编译错误!不知道每次跳多少字节

GCC 扩展允许void*的算术(当成char*),但标准 C 不允许。要移动void*,先转成char*


二、void *的典型用途:通用接口

尽管有这些限制,void *却成为了 C 语言实现“泛型”的基石。标准库中大量函数都使用它:

函数用途
malloc/calloc/realloc返回void*,可赋给任何指针
memcpy/memset/memmove操作任意类型的内存块
qsort/bsearch排序和查找任何类型的数据
线程创建函数传递任意类型的参数

它们的共同点是:操作的是“一段内存”,不关心里面存的是什么类型。只要我们告诉它这块内存的起始地址大小,剩下的事情就能做。


三、实现一个通用的swap函数

以前我们写的swap只能交换两个int。用void*加上元素大小参数,就能交换任意类型的值。

#include<stdio.h>#include<string.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){intx=10,y=20;swap_generic(&x,&y,sizeof(int));printf("x=%d, y=%d\n",x,y);// x=20, y=10doubled1=1.5,d2=3.5;swap_generic(&d1,&d2,sizeof(double));printf("d1=%.1f, d2=%.1f\n",d1,d2);// d1=3.5, d2=1.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

#include<stdio.h>#include<string.h>#include<stddef.h>// 交换 size 字节的两块内存staticvoidswap_bytes(void*a,void*b,size_tsize){char*pa=(char*)a;char*pb=(char*)b;for(size_ti=0;i<size;i++){chartemp=pa[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_ti=0;i<nmemb-1;i++){intswapped=0;for(size_tj=0;j<nmemb-1-i;j++){// 计算相邻元素的地址void*elem_j=arr+j*size;void*elem_next=arr+(j+1)*size;if(compar(elem_j,elem_next)>0){swap_bytes(elem_j,elem_next,size);swapped=1;}}if(!swapped)break;}}

核心点:

  • base被转成char*,这样arr + j * size就能准确定位到第j个元素的起始地址。
  • 调用者提供的compar负责比较逻辑,它拿到的是两个void*,内部再转成实际类型比较。

五、使用我们的泛型排序:对intdouble、结构体排序

1. 排序int数组

intcompare_int(constvoid*a,constvoid*b){intia=*(constint*)a;intib=*(constint*)b;return(ia>ib)-(ia<ib);// 简洁的差值法,避免溢出}intmain(void){intarr[]={5,2,9,1,5,6};size_tn=sizeof(arr)/sizeof(arr[0]);generic_bubble_sort(arr,n,sizeof(int),compare_int);for(size_ti=0;i<n;i++)printf("%d ",arr[i]);printf("\n");return0;}

2. 排序double数组

intcompare_double(constvoid*a,constvoid*b){doubleda=*(constdouble*)a;doubledb=*(constdouble*)b;if(da<db)return-1;if(da>db)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->score<sb->score)return-1;if(sa->score>sb->score)return1;return0;}intmain(void){Student class[]={{"Alice",1,92.5},{"Bob",2,85.0},{"Carol",3,78.5},};size_tn=sizeof(class)/sizeof(class[0]);generic_bubble_sort(class,n,sizeof(Student),compare_student_by_score);for(size_ti=0;i<n;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*和动态内存,构建一个经典的动态数据结构——单向链表,让数据不在编译时固定大小,而是可以自由地增长和收缩。真正的数据结构之旅,现在开始。


课后小练习

  1. 实现一个泛型的数组逆置函数void generic_reverse(void *base, size_t nmemb, size_t size),将数组中元素的顺序颠倒。用它逆置一个int数组和一个double数组,验证结果。
  2. 利用qsort(标准库函数)对字符串数组char *words[] = {"banana", "apple", "cherry"};按字典序排序并打印。提示:比较函数中解引用void*得到char**,再用strcmp
  3. 实现一个泛型的find_max函数:void *find_max(const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)),返回指向最大元素的指针。测试对int数组和struct Student数组的查找。
  4. (小挑战)用void*和函数指针设计一个简单的回调式foreach函数:void array_foreach(void *base, size_t nmemb, size_t size, void (*func)(void *))。该函数遍历数组,对每个元素调用func。写一个打印intdouble的回调,并在main中测试。

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库。

http://www.jsqmd.com/news/1135360/

相关文章:

  • 编排模式:中央协调 + 子任务分配
  • deepseek 导出遇格式难题?AI 导出鸭轻松实现高效无损文档导出
  • AI 复制内容带乱码太糟心?AI 导出鸭一键解决格式错乱与乱码难题
  • 云原生 AI 推理弹性:GPU 扩容慢,要先设计预热池
  • 让老旧安卓电视焕发新生:MyTV-Android开源电视直播应用完全指南
  • 做 excel 表格用哪个 deepseek 软件文档导出不用反复调整?AI 导出鸭稳定高效,完美适配 Excel 生成与导出需求
  • 个人介绍及学习目标
  • 【AAAI 2026】VQAThinker:通过RL进行可解释VQA训练
  • LRU Cache:面试必考设计题
  • 深度探索C++对象模型 学习笔记 第七章 站在对象模型的尖端(1)
  • 校园一卡通安全策略研究​​​​​​​
  • 向下普及L2+、向上攻坚L4,被技术与商业双重夹击的L3自动驾驶何去何从
  • 5分钟搭建终极DNS转发器:mosdns完整实战指南
  • RocketMQ 基础知识全解析:从入门到理解
  • Audrey MCP 服务说明文档
  • Windows平台HEIC格式转换终极指南:HEIF Utility完整解决方案
  • 极简 API 网关:手写一个支持限流和断路器的轻量反向代理
  • 实验1 RIP实验报告
  • Xiaomi Cloud Tokens Extractor:解锁小米智能家居设备的专业令牌提取工具
  • 用 grill-me 推导盘前简报 MVP:12 题钉边界 + schema 收束
  • Kindle Comic Converter终极指南:三步打造完美Kindle漫画阅读体验
  • 高空气象探空站目录采集:用 Python 把编号型站点资料规整入库
  • 【intra-mart】相关知识(NTT DATA 公司开发的产品)
  • 模板基础与 SFINAE
  • 科大讯飞X3办公本:专为纸感办公设计的智能手写终端
  • 栈(数据结构)
  • 路由模式:智能分发 + 垂直 Agent
  • 树的相关算法
  • git的分支介绍
  • 【项目编号 project00090】SpringBoot大学校历系统:校历编排、日程提醒与后台管理一体化实战