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*的威力——它让函数变得类型无关

核心思想:通用函数把数据当成“无类型的内存块”来处理。它不关心你存的是什么,只关心这块内存有多大。具体怎么解释,由调用者自己负责。

标准库里的qsortbsearch也是这个套路。它们的参数里大量使用void*,配合函数指针,实现了真正的通用算法。

下面我用一张图来总结void*的核心逻辑:

void* 核心逻辑图 void* 万能指针 定义:无类型指针 只存地址,不存类型 使用:强制类型转换 转成具体类型后再解引用 应用:通用函数 swap、qsort、malloc ⚠️ 注意事项 • 不能直接解引用 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*是无类型指针,可以指向任何类型的数据
  • 不能直接解引用,必须先强制转换成具体类型
  • 它是实现通用函数(如swapqsort)的关键
  • 使用时要格外小心类型匹配和内存对齐

说实话,void*是C语言里最接近“泛型”的东西。虽然它不像C++的模板那么安全,但胜在轻量、底层、高效。在嵌入式领域,它几乎是绕不开的。你把它用好了,写出来的代码会非常灵活;用不好,那就是给自己挖坑。

嗯,下一节我们会讲函数指针,那又是另一个有意思的话题了。


公众号:蓝海资料掘金营,微信deep3321