12、void指针:void*的定义与使用、void*的强制类型转换、void*在通用函数中的应用
各位同学,今天我们来聊聊C语言里一个非常特殊、也特别容易让人迷惑的类型——void*,也就是void指针。
我刚开始学C语言那会儿,看到void就以为是“空”、“没有”的意思。那void*不就是“空指针”吗?其实不是。它真正的含义是——无类型指针。说白了,就是一个指针,但它不知道自己指向的数据是什么类型。
你想想看,这玩意儿有什么用?
嗯,用处大了去了。很多底层系统、通用库、内核代码里,void*几乎是标配。我当年在做一个嵌入式通信协议栈的时候,就大量用到了它。没有它,很多通用接口根本写不出来。
12.1 void*的定义与基本使用
void*的定义很简单:
void *ptr;
这就声明了一个void指针。它可以指向任何类型的数据。比如:
int a = 100;
float b = 3.14;
char c = 'X';
void *vp;
vp = &a; // 指向int
vp = &b; // 指向float
vp = &c; // 指向char
看到了吗?同一个vp,今天指向int,明天指向float,完全没问题。这在普通指针里是做不到的——你不能把一个int*直接赋给float*,编译器会报警告。但void*是“万能插座”,什么插头都能插。
核心要点:void*只保存地址,不保存类型信息。它就像一个快递单号,只知道包裹在哪儿,不知道里面装的是书还是衣服。
但是,这里有个大坑——你不能直接对void*解引用。为什么?因为编译器不知道它指向的数据占几个字节。你写*vp,编译器会懵掉:你到底想读几个字节?
void *vp;
int a = 100;
vp = &a;
// *vp = 200; // 错误!不能解引用void*
// vp++; // 错误!不能对void*做指针运算
我刚开始犯过这个错,编译报错还一脸懵。后来才明白,void*就是个“盲人摸象”——你知道大象在哪儿,但不知道它长啥样。
12.2 void*的强制类型转换
既然不能直接解引用,那怎么用?答案是——强制类型转换。
把void*转成具体的类型指针,然后再解引用。就像这样:
void *vp;
int a = 100;
vp = &a;
// 强制转换成int*,再解引用
int *ip = (int *)vp;
printf("%d\n", *ip); // 输出100
这里(int *)vp就是把void指针“恢复”成int指针。我习惯把这个过程叫做“类型还原”。
你可能会问:为什么不直接用int*,非要绕一圈?
好问题。直接能用int*当然最好。但当你写通用函数的时候,你事先不知道调用者会传什么类型的指针进来。这时候void*就是唯一的解。
个人习惯:我在做强制类型转换时,一定会先确认这个void指针原本指向的是什么类型。如果乱转,比如把int*转成float*来解引用,数据会完全错乱。我曾经在调试一个bug时花了整整一下午,最后发现就是类型转错了。
另外,C语言中malloc返回的就是void*:
int *arr = (int *)malloc(10 * sizeof(int));
这里(int *)就是强制类型转换。虽然C语言允许隐式转换(不用写(int *)),但我建议你永远显式写出来。一是代码更清晰,二是在C++中必须写。
12.3 void*在通用函数中的应用
这是void*最精彩的部分。我们来看一个经典例子——通用的交换函数。
假设你要写一个交换两个整数的函数:
void swap_int(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
没问题。但如果要交换两个double呢?再写一个swap_double?交换结构体呢?那得写多少个版本?
用void*,一个函数搞定所有类型:
void swap(void *a, void *b, size_t size) {
// 在栈上分配临时空间
char tmp[size]; // C99支持变长数组
// 按字节拷贝
memcpy(tmp, a, size);
memcpy(a, b, size);
memcpy(b, tmp, size);
}
调用时:
int x = 10, y = 20;
swap(&x, &y, sizeof(int));
double d1 = 1.5, d2 = 2.5;
swap(&d1, &d2, sizeof(double));
看到了吗?同一个swap函数,既能交换int,又能交换double。这就是void*的威力——它让函数变得类型无关。
核心思想:通用函数把数据当成“无类型的内存块”来处理。它不关心你存的是什么,只关心这块内存有多大。具体怎么解释,由调用者自己负责。
标准库里的qsort、bsearch也是这个套路。它们的参数里大量使用void*,配合函数指针,实现了真正的通用算法。
下面我用一张图来总结void*的核心逻辑:
12.4 避坑指南
讲了这么多好处,我也得说说void*的坑。毕竟我在项目里被它坑过不止一次。
我曾经接手过一个老项目,里面有个回调函数参数全是void*。调用者传了一个结构体指针进去,回调函数里直接强转成另一个结构体类型。结果两个结构体成员顺序不一样,数据全读错了。这个bug查了三天。
所以,使用void*时请记住这几条:
- 类型信息不能丢:谁把
void*传出去的,谁就要负责在另一端把它转回正确的类型。最好在文档或注释里写明原始类型。 - 不要对void*做指针运算:C标准规定,
void*不能做加减操作。因为不知道步长。虽然GCC把它当作char*处理(步长为1),但这是扩展行为,不要依赖。 - NULL和void*是两码事:
NULL表示指针不指向任何地方。void*表示指针指向某个地方,但不知道类型。别搞混了。 - 强制转换时注意对齐:有些架构(比如ARM)对对齐要求很严格。把一个
int*转成char*没问题,但反过来,如果char*地址不是4的倍数,转成int*再解引用可能会崩溃。
我的建议:能用具体类型指针的时候,尽量用具体类型。void*是工具,不是玩具。只有在写通用接口、回调函数、底层内存操作时,才搬出它来。滥用void*会让代码变得难以维护。
12.5 小结
好了,关于void*,我们就讲到这里。总结一下:
void*是无类型指针,可以指向任何类型的数据- 不能直接解引用,必须先强制转换成具体类型
- 它是实现通用函数(如
swap、qsort)的关键 - 使用时要格外小心类型匹配和内存对齐
说实话,void*是C语言里最接近“泛型”的东西。虽然它不像C++的模板那么安全,但胜在轻量、底层、高效。在嵌入式领域,它几乎是绕不开的。你把它用好了,写出来的代码会非常灵活;用不好,那就是给自己挖坑。
嗯,下一节我们会讲函数指针,那又是另一个有意思的话题了。
公众号:蓝海资料掘金营,微信deep3321