为啥C/C++高手都绕不开void*?新手看完:原来我之前都在“瞎写”!

你是不是也有过这种“卡壳时刻”?学C/C++时,从int、char写到数组、结构体,一路顺风顺水,直到屏幕上蹦出个void*——瞬间脑袋里飘满问号:“无类型指针”是啥?既不能直接解引用,又不能随便做运算,这玩意儿到底有啥用?

想绕开它吧,翻高手写的通用库(比如mallocqsort),里面全是void*;去面试,面试官又盯着问“怎么用void*写个通用链表”;自己写代码,处理int要写一个函数,处理float又要写一个,重复得像“复制粘贴机器”……

其实啊,void*根本不是“拦路虎”,而是C/C++里的“万能工具”——新手觉得它抽象,是因为没摸清它的“脾气”;高手离不开它,是因为靠它能搞定内存管理、泛型编程这些“高阶操作”。今天咱们就把void*扒得明明白白,用大白话+小比喻,保证你看完直呼“原来这么简单!”

一、先搞懂:void*到底是个啥?

先从void说起——C/C++里void表示“无类型”,比如void func()就是“没有返回值的函数”。那void*,顾名思义就是“指向无类型数据的指针”。

更通俗点说,void*就是一个“纯纯的内存地址”:它只知道“我指向内存里的某个位置”,但完全不清楚“这个位置存的是int、float还是结构体”,也不知道“这堆数据占几个字节”。

咱们拿“停车场”打个比方就懂了:

  • int*像“小轿车专属停车证”:只能对应小轿车的车位,你拿它去停摩托车,管理员(编译器)直接摆手说“不行”;
  • char*像“摩托车停车证”:只能停摩托车,别的车也用不了;
  • void*,就是一张“空白停车证”!不管你是小轿车(int)、摩托车(char),还是大卡车(结构体),都能拿这张证占车位。但有个规矩:你要用车的时候,必须在空白证上写清楚“这是啥车”(也就是类型转换),不然管理员不知道咋帮你找车——这就是void*的核心逻辑:“啥都能装,但用的时候得说清楚装的是啥”。

二、void*的3个“怪脾气”:新手必踩的坑先避开

void*好用,但有几个“脾气”得摸清,不然写代码时编译器能把你“怼到怀疑人生”。

1. 它是“万能收纳盒”:啥指针都能装

void*最大的优点就是“不挑类型”——不管是int、float的指针,还是结构体指针,都能直接赋值给它,不用额外操作。

比如这样写,编译器完全不报错:

int a = 10;      // 小轿车
float b = 3.14;  // 摩托车
void *p = &a;    // 空白证先装小轿车地址
p = &b;          // 换个摩托车地址,照样装

就像你的收纳盒既能放笔,又能放橡皮,不用为了不同东西买多个盒子——这就是void*的“通用性”。

2. 不能“直接动手”:解引用和算术运算都要“先打招呼”

void*虽然能装所有指针,但不能直接“用”——这是新手最常踩的坑,咱们分两点说:

(1)想解引用?先做“类型转换”

解引用就是“把内存里的数据取出来”,但void*不知道数据类型,就像你拿着空白停车证,管理员不知道你要开啥车,没法帮你找——所以必须先把void*转成具体类型的指针,才能解引用。

比如下面这段代码,直接解引用p会报错,转换后就没问题:

int a = 10;
void *p = &a;        // 空白证装了小轿车地址
// *p = 20;          // 错误!没说清是啥车,没法取
int *m = (int *)p;   // 给空白证写清楚“是小轿车”(类型转换)
*m = 20;             // 正确!现在能取数据了,a变成20

这里还要吐槽下C和C++的“小别扭”:

  • C语言比较“宽松”:把void*转给其他指针(比如int* m = p),编译器可能不报错(但严格模式会警告);
  • C++特别“较真”:必须写清楚(int*)p,不然直接报错,说“转换无效”。

但不管是C还是C++,咱都建议你“显式转换”——就像说话别含糊,明明白白写清楚,后面看代码的人(包括3天后的你)才不会骂“这谁写的烂代码”。

(2)想做算术运算?先转成具体类型

比如你想让指针“往后挪一位”(p++),void*也不允许——因为它不知道“挪几位”:char是1字节,int是4字节,没说清类型,编译器咋知道挪多少?

这里有个小例外:GNU编译器(比如GCC)有个“扩展功能”,默认把void*char*p++就挪1字节。但千万别依赖这个!一旦换个编译器(比如VS),代码直接报错,移植性全没了。

正确做法是:先转成具体类型,再做运算。比如想让int指针挪一位:

void *p = malloc(100);  // 分配100字节内存
// p++;                // 错误!ANSI C不允许
int *ip = (int *)p;     // 先转成int*
ip++;                   // 正确!挪4字节(int的大小)

3. 类型安全靠“自觉”:编译器不帮你“查错”

void*没有“类型检查”——你把int指针转成float指针,编译器可能不拦你,但运行时会出大问题,这就是“未定义行为”。

比如下面这段代码,看着没报错,但打印结果会是一堆乱码:

int x = 65;        // 65是字符'A'的ASCII值
void *p = &x;
float *fp = (float *)p;  // 错误!把小轿车硬说成摩托车
printf("%f\n", *fp);     // 结果可能是0.000000或者乱码

就像你把苹果硬说成橘子,虽然能装到橘子筐里,但吃的时候肯定不对味——void*的类型转换,必须“存的是啥,就转成啥”,不能瞎来。

三、void*的4个“大用处”:高手都靠它写“通用代码”

为啥高手离不开void*?因为它能解决“重复造轮子”的问题——用它写的代码,能适配所有类型,不用为了int、float各写一套。

1. 写“万能函数”:一个函数处理所有类型数据

比如你想写个“打印数据”的函数,没有void*的话,得写两个函数:

// 打印int
void print_int(int *data) { printf("%d\n", *data); }
// 打印float
void print_float(float *data) { printf("%.2f\n", *data); }

要是再来个double、char,岂不是要写到手软?有了void*,一个函数就搞定:

// 通用打印函数:type=1是int,type=2是float
void print_data(void *data, int type) {
    if (type == 1) {
        int *int_data = (int *)data;  // 转int*
        printf("%d\n", *int_data);
    } else if (type == 2) {
        float *float_data = (float *)data;  // 转float*
        printf("%.2f\n", *float_data);
    }
}

// 用的时候直接传不同类型
int a = 10;
float b = 3.14;
print_data(&a, 1);  // 打印10
print_data(&b, 2);  // 打印3.14

这就是“泛型编程”的雏形——用void*让函数变“万能”,不用重复写代码。

2. 造“通用数据结构”:链表、队列能存所有类型

比如你想写个链表,没有void*的话,存int要写int* data,存char要写char* data,得搞N个链表结构。

有了void*,一个链表就能存所有类型:

// 通用链表节点:data能装任何类型
struct Node {
    void *data;       // 万能数据域
    struct Node *next; // 下一个节点
};

// 存int
int a = 10;
struct Node node1;
node1.data = &a;

// 存字符串
char str[] = "hello";
struct Node node2;
node2.data = str;

就像一个“万能货架”,既能放苹果,又能放书本——这就是void*在数据结构里的价值,大大减少代码冗余。

3. 标准库的“核心工具”:mallocmemcpy都靠它

你平时用的malloc(分配内存)、memcpy(拷贝内存),返回值或参数都是void*——因为它们不用管你存啥类型,只需要处理“内存块”就行。

比如malloc(100):分配100字节内存,返回void*,你想存int就转int*,想存float就转float*,特别灵活。

// 分配100字节,存int
void *ptr = malloc(100);
int *int_ptr = (int *)ptr;
int_ptr[0] = 42;  // 存第一个int

// 拷贝内存:把src的100字节拷到ptr
memcpy(ptr, src, 100);  // 不用管src是啥类型,只拷内存块

要是没有void*malloc得返回int*float*等N种类型,根本没法实现——所以void*是标准库的“基石”。

4. 跨场景传数据:多线程、回调函数的“桥梁”

比如写多线程代码时,线程函数pthread_create的参数只能是void*——不管你要传int、结构体,都得用void*包一下,线程里再转回来。

示例代码:

// 要传的结构体(比如任务信息)
struct Task {
    int id;
    char name[20];
};

// 线程函数:参数是void*
void *thread_func(void *arg) {
    // 转成结构体指针,才能用
    struct Task *task = (struct Task *)arg;
    printf("线程处理任务:%d,%s\n", task->id, task->name);
    return NULL;
}

// 主函数里传数据
int main() {
    pthread_t tid;
    struct Task task = {1, "打印日志"};
    // 把结构体指针转成void*传给线程
    pthread_create(&tid, NULL, thread_func, (void *)&task);
    pthread_join(tid, NULL);
    return 0;
}

回调函数也一样——比如你写个排序函数,想让用户自己定义“怎么比大小”,就用void*传用户数据,灵活又通用。

四、用void*的5个“保命规范”:别踩坑!

void*好用,但要是不注意规范,很容易写出“bug代码”——记住这5条,能帮你避开90%的坑。

1. 赋值:“装”的时候随便装,“取”的时候必须转

  • 把其他指针给void*:直接赋值(比如void* p = &a),不用转;
  • void*给其他指针:必须显式转换(比如int* m = (int*)p),别偷懒。

2. 解引用:“转对类型”再动手

永远记住:void*不能直接解引用,必须先转成和原数据一致的类型——比如存的是int,就转int*;存的是结构体,就转结构体指针,别乱转。

3. 算术运算:“先转类型”再挪位

不管是p++还是p+=2,先把void*转成具体类型(比如int*char*),再做运算——别依赖GCC的扩展,不然代码移植时会崩。

4. 内存:“谁分配,谁释放”

void*指向的内存(比如malloc出来的),必须手动释放——不然会“内存泄漏”,就像占了车位不挪车,时间长了内存不够用,程序会卡死。

示例:

void *p = malloc(100);  // 分配内存
// 用完一定要释放
free(p);
p = NULL;  // 释放后把指针置空,避免“悬空指针”

5. 可移植性:别依赖编译器“小脾气”

ANSI C(标准C)不允许void*直接算术运算,也不允许void*隐式转其他指针——不管用啥编译器,都按标准写,别图省事用扩展功能,不然换个环境代码就报错。

五、典型场景汇总:void*到底在哪用?

为了让你更直观,我整理了void*的常见用法,一看就懂:

应用场景 示例代码片段 作用说明
内存操作函数 void *memcpy(void *dest, const void *src, size_t n); 拷贝任意类型的内存块,不用管数据类型
通用链表 struct Node { void *data; struct Node *next; }; 一个链表能存int、char、结构体等所有类型
回调函数参数 void callback(void *user_data); 传递用户自定义数据,比如排序时的比较规则
多线程传参 pthread_create(&tid, NULL, func, (void *)&arg); 给线程传任意类型数据,线程内再转回来
泛型排序 qsort(base, nmemb, size, compar); 对int、float、结构体数组都能排序

六、总结:void*其实是“进阶钥匙”

最后咱们用一张表,把void*的核心点说透,以后写代码不用再慌:

特性 大白话描述
本质 纯纯的内存地址,没带“数据类型标签”
核心能力 啥类型的指针都能装,是“万能收纳盒”
使用前提 必须先贴好“类型标签”(显式转换),才能用
主要用途 1. 写通用函数/数据结构;2. 标准库(malloc、memcpy);3. 多线程/回调函数传参;4. 底层内存操作
核心风险 贴错“类型标签”(乱转换),会导致程序出问题

其实void*一点都不抽象——它就是C/C++给你的“万能工具”,帮你摆脱“重复造轮子”的麻烦,写出更灵活、更通用的代码。以前觉得它难,只是没摸清它的“脾气”;现在搞懂了,下次写通用链表、多线程传参时,就能用void*轻松搞定,离“C/C++高手”又近了一步!

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐