不懂 void *,也敢说是C/C++高手?

120 阅读9分钟

<> 博客简介: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_”,微信名"骠姚校尉"。