<> 博客简介:Linux、rtos、物联网、AIoT、音视频开发、C/C++、arm、stm32等芯片,嵌入式高级工程师、架构师,日常技术干货、职场经验分享~
<> 公众号:嵌入式技术部落,关注公众号,学习更多嵌入式干货
一、前言
不懂 void *,也敢说是 C/C++ 高手?这话真不是夸张 —— 在 C/C++ 里,void *就像 “进阶门票”,新手觉得它抽象难用,高手却靠它实现灵活的内存管理、泛型编程和底层交互。要是绕开void*,别说写通用库、做系统开发,连面试里的基础题都可能卡壳。
void * 是 C/C++ 语言中一种非常基础且强大的工具,理解它对于掌握内存管理、通用编程和系统级编程至关重要。
二、void * 是什么
void 在C/C++语言中通常表示“无类型”。因此,void* 可以理解为“指向一个无类型数据的指针”。
void * 特殊之处在于它可以存储任何数据类型的地址。 无论是 int、char、float、struct,它们的地址都可以赋给一个 void* 类型的指针变量。它不“知道”它指向的数据的类型和大小。 正因为是“无类型”, void 本身没有具体的数据信息,void * 并不直接知道它所指向的数据的类型和大小。
void * 被定义为 “指向任何类型数据的指针” 或 “通用指针”。
可以将其理解为一个 “纯内存地址”,它只记录了一个内存位置的起点,但没有任何关于“这个地址上存放的数据类型和数据大小”的信息。
所以void* 类型指针,我们常称之为通用指针或万能指针或泛型指针。
简单来说,void * 就是个"啥都能指,但啥都不知道"的指针。void * 就像个"万能容器",能装下任何类型的指针,但用的时候必须明确告诉编译器"里面装的是什么"。
如何明确告诉编译器"里面装的是什么"呢?是通过类型转换,接着看下文我们详细总结。
把内存想象成一个大型停车场
- int * 就像是一辆小轿车的停车证,只能停放小轿车。
- char * 就像是一辆摩托车的停车证,只能停放摩托车。
- void * 就像是一张空白停车证,爱停什么车自己决定。
- 类型转换就像在空白停车证上写上车型。
- 解引用就是把车用车位开出来。
三、void * 的特点
1、无类型指针
void *是通用指针类型,不关联具体数据类型,可指向任意类型的数据,就是说可以用任意类型的指针对 void *指针赋值。
int a = 10;
float b = 3.14;
void *p = &a; // 指向int
p = &b; // 指向float
2、类型安全性限制
【1】不能直接解引用
编译器不知道 void * 指向的数据的大小和类型,因此不能直接对其进行解引用 (*) 操作,需强制转换为具体类型指针后操作。
int a = 10;
void *p = &a; // 合法,无需显式转换
int *m = (int *)p; // 必须转换后才能访问
*m = 20; // 正确操作
C++要求从 void* 到其他指针类型的赋值必须显式转换。但在 C语言 中,void* 可以隐式地转换为任何其他数据指针类型。但是为了代码清晰,提高代码的类型安全性、可移植性以及更易维护,避免潜在的错误,显式转换是更好的实践,也是更专业的操作。下面我们分别用C和C++代码示例:
C代码
#include <stdio.h>
int main(int argc, char *argv[])
{
int a = 10;
void *p = &a;
int *m = p;
printf("C *m = %d\n", *m);
return 0;
}
编译结果
编译通过且没报错,但是在严格模式下可能产生警告。
C++代码
#include <iostream>
int main(int argc, char *argv[])
{
int a = 10;
void *p = &a;
int *m = p;
std::cout << "C++ *m = " << *m << std::endl;
return 0;
}
编译结果
有报错。
还是那句话,无论C还是C++,为了代码清晰,提高代码的类型安全性、可移植性以及更易维护,避免潜在的错误,显式转换是更好的实践,也是更专业的操作。
【2】不能直接进行指针运算
这里分两种情况
标准C (ANSI C及后续)
在 ANSI C 标准中,不允许对 void* 指针进行一些算术运算如 p++ 或 p+=1 等,因为既然 void 是无类型,那么每次算术运算我们就不知道该操作几个字节,例如 char 型操作 sizeof(char) 字节,而 int 则要操作 sizeof(int) 字节。
void *p = malloc(100);
p++; // 错误:对 void* 指针进行算术运算无效
GNU C 扩展 (GCC)
而在 GNU 中则允许,因为在默认情况下,GNU 认为 void * 和 char * 一样,对 void * 进行 +1 操作,会使其前进 1 个字节。既然是确定的,当然可以进行一些算术操作。
void *p = malloc(100);
p++; // GNU扩展:p 的值增加了 1 (字节)
重要:为了编写可移植的代码,永远不要直接对 void * 进行算术运算。正确的做法是先将其进行类型转换,然后再进行运算。
四、void * 的用途
1.实现泛型编程
【1】 函数参数通用化
允许函数接受任意类型指针,如回调函数。
void process_data(void *data) {
int *int_data = (int *)data; // 转换为具体类型后处理
printf("%d\n", *int_data);
}
【2】数据结构通用化
实现通用数据结构(链表、队列)。例如,你可以构建一个链表,其节点存储 void* 类型的数据,这样链表就可以用来存储任何类型的数据。
struct Node {
void *data; // 存储任意类型数据
struct Node *next;
};
【3】通用内存操作
标准库函数如 malloc、free、memcpy、memset均使用 void *,以支持任意类型内存的分配和操作。
void *ptr = malloc(100); // 分配 100 字节内存,可存储任意类型数据
memcpy(ptr, src, 100); // 复制内存块,不依赖具体类型
2.跨类型数据传递
在异构数据交互中(如多线程、回调函数),void *可封装不同类型数据,传递时保持灵活性。
void thread_func(void *arg) {
struct Data *d = (struct Data *)arg; // 转换为具体结构体
// 处理数据
}
3. 与硬件/系统交互
直接操作内存地址时(如嵌入式开发、驱动编程),void *可表示任意硬件寄存器或内存映射区域。
五、关键使用规范与陷阱
赋值操作 - 与其他指针类型的相互转换
void * 可以隐式接收任何数据指针类型的赋值。反之,void * 也可以赋值给任何数据指针类型,但是需要显式类型转换。
int x = 10;
int *int_ptr = &x;
void *void_ptr = NULL;
// 正确:任何指针可以隐式转为 void*
void_ptr = int_ptr; // OK
void_ptr = &x; // OK
// 需要显式转换
int_ptr = (int *)void_ptr; // 要求显式转换
类型转换 - 在使用数据前的必要步骤
这是 void * 最主要的使用场景。由于 void * 不包含类型信息,不能直接解引用 void *,在使用 void * 指向的数据前,必须转换为具体的指针类型。
void *data = malloc(sizeof(int) * 10);
// 必须转换为具体类型才能使用
int *numbers = (int *)data;
numbers[0] = 42;
// 或者直接转换使用
*(int *)data = 100;
比较操作 - 允许与其他指针比较
void * 可以与其他指针进行比较操作,比较的是内存地址值。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[3];
void *void_ptr = ptr1;
// 相等性比较
if (void_ptr == ptr1) {
printf("指向相同地址\n");
}
// 不等比较
if (void_ptr != ptr2) {
printf("指向不同地址\n");
}
条件运算 - 检查是否为 NULL
void * 可以像其他指针一样在条件语句中检查有效性。
void process_data(void *data) {
// 检查指针是否为 NULL
if (data == NULL) {
printf("错误: 接收到空指针\n");
return;
}
// 或者更简洁的写法
if (!data) {
printf("错误: 接收到空指针\n");
return;
}
// 处理数据...
}
指针运算
ANSI C标准,禁止对void *直接算术运算(如p++)。原因void *无类型信息,编译器无法确定步长。
GNU扩展,允许运算(视作char *,步长为1字节)。风险是降低代码可移植性。
最佳实践,总是先转换为具体类型指针再进行算术运算。
此内容上文已详细总结。
六、典型应用场景
| 场景 | 示例代码片段 | 作用说明 |
|---|---|---|
| 内存操作函数 | void *memcpy(void *dest, const void *src, size_t n); | 通用内存拷贝,不依赖数据类型 |
| 链表节点 | struct Node { void *data; struct Node *next; }; | 存储任意类型数据 |
| 回调函数参数 | void callback(void *user_data); | 传递用户自定义数据 |
| 多线程参数传递 | pthread_create(&tid, NULL, thread_func, (void *)arg); | 线程间传递参数 |
| 泛型排序函数 | qsort(base, nmemb, size, compar); | 对任意类型数组排序 |
七、注意事项
1.类型转换风险
错误转换可能导致数据解释错误(如将 int *转为 float *),需严格保证类型一致性。
int x = 65;
void *p = &x;
// 错误:错误解释数据
// float *fp = (float *)p;
// printf("%f\n", *fp); // 未定义行为
// 正确:保持类型一致
int *ip = (int *)p;
printf("%d\n", *ip); // 输出 65
2.内存生命周期管理 void *指向的内存需由调用者负责释放,避免内存泄漏或悬空指针。
3.可移植性差异 ANSI C 标准禁止对 void *直接进行算术运算,但 GNU 编译器允许(需显式转换为 char *以兼容标准)。
八、总结
void *总结如下
| 特性 | 描述 |
|---|---|
| 本质 | 一个纯内存地址,不携带类型信息。 |
| 核心能力 | 容纳任何数据类型的指针。 |
| 使用前提 | 必须通过显式类型转换“还原”为具体类型后才能解引用或进行指针运算。 |
| 主要用途 | 1. 内存管理函数 (malloc, free)。 2. 通用算法函数 (qsort, bsearch)。 3. 线程参数/返回值、回调函数上下文。 4. 底层内存操作 (memcpy)。 |
| 核心风险 | 类型安全缺失,错误的类型转换会导致未定义行为。 |
九、写在最后
由于水平有限,纰漏之处、逻辑不甚严谨之处,望看官们不吝指正。如能给您带来些许帮助,更是不胜荣幸。
欢迎加入嵌入式技术交流群,关注公众号或添加下方作者微信申请入群。 作者微信 “_PiaoYaoXiaoWei_”,微信名"骠姚校尉"。