void* 在 C 和 C++ 中被称为“无类型指针”或“通用指针”(generic pointer)。它是一种特殊的指针类型,可以指向任何数据类型的对象(或函数)的地址,但它本身不包含任何关于它所指向对象类型的信息。
void* 的主要用途和使用方式包括:
- 通用函数接口(如内存操作函数):
标准库函数如 malloc, calloc, realloc 返回 void*,因为它们分配的是原始内存块,并不知道你打算在这块内存中存储什么类型的数据。你需要将返回的 void* 显式转换(cast)为你需要的具体指针类型才能使用。
memcpy, memmove, memset 等函数接受 void* 参数,因为它们按字节操作内存,不关心实际的数据类型,只需要知道内存地址和大小。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 1. malloc example
int *p_int = (int*)malloc(sizeof(int)); // malloc returns void*, cast to int*
if (p_int == NULL) {
perror("Failed to allocate memory");
return 1;
}
*p_int = 100;
printf("Value via p_int: %d\n", *p_int);
free(p_int); // Always free allocated memory
// 2. memcpy example
char src[] = "Hello";
char dest[10];
memcpy(dest, src, strlen(src) + 1); // void* dest, const void* src, size_t n
// Implicit conversion from char* to void* is allowed
printf("Copied string: %s\n", dest);
return 0;
}
- 实现泛型数据结构和算法 (主要在 C 语言中):
在 C 语言中,没有模板(templates)这样的泛型编程机制。如果想创建可以存储任何类型数据的列表、树、哈希表等,或者编写可以处理任何类型数组的排序、搜索算法(如标准库的 qsort),void* 是常用的方法。
数据结构通常会存储 void* 指向实际数据。
像 qsort 这样的函数接受一个 void* 指向数组基地址,并需要一个比较函数,该比较函数也接受两个 const void* 参数,你需要在比较函数内部将 void* 转换回实际的数据类型指针进行比较。
#include <stdio.h>
#include <stdlib.h>
// Comparison function for qsort (sorting integers)
int compare_ints(const void *a, const void *b) {
int int_a = *((const int*)a); // Cast void* to const int* and dereference
int int_b = *((const int*)b); // Cast void* to const int* and dereference
if (int_a < int_b) return -1;
if (int_a > int_b) return 1;
return 0;
// Or simply: return (int_a > int_b) - (int_a < int_b);
}
int main() {
int numbers[] = {5, 2, 8, 1, 9, 4};
size_t num_count = sizeof(numbers) / sizeof(numbers[0]);
printf("Before sorting: ");
for (size_t i = 0; i < num_count; ++i) printf("%d ", numbers[i]);
printf("\n");
// Use qsort with void* base address and comparison function
qsort(numbers, num_count, sizeof(int), compare_ints);
printf("After sorting: ");
for (size_t i = 0; i < num_count; ++i) printf("%d ", numbers[i]);
printf("\n");
return 0;
}
- 传递不透明数据指针:
在库或 API 设计中,有时会将内部结构的指针作为 void* 返回给用户,用户不能(也不应该)直接操作它,只能将其传递回库的其他函数。这隐藏了实现细节。
- 回调函数的用户数据(User Data / Context):
许多 API(如图形库、线程库、事件处理系统)允许你注册一个回调函数,并在注册时提供一个 void* 参数(通常称为 userData、context 或类似名称)。当 API 调用你的回调函数时,它会把你当初提供的 void* 再传回给你的回调函数。这允许你的回调函数访问特定的上下文信息,而 API 本身无需知道这些信息的具体类型。
假设有一个库函数 setTimer,它会在指定的毫秒数后调用你提供的回调函数。为了让你的回调函数知道是哪个计时器触发了(或者携带任何你想传递的信息),setTimer 允许你传递一个 void* 用户数据。
#include <stdio.h> // 包含标准输入输出头文件
#include <stdlib.h> // 包含标准库头文件 (用于 NULL)
// --- 假设这是外部库的一部分 ---
typedef void (*TimerCallback)(int timerId, void* userData);
// 模拟设置一个定时器。
void setTimer(int milliseconds, TimerCallback callback, void* userData) {
printf("定时器库:正在设置 %d 毫秒的定时器。\n", milliseconds);
// --- 想象等待 'milliseconds' 毫秒 ---
printf("定时器库:定时器到期!调用回调函数。\n");
// 库函数不知道也不关心 userData 指向什么,
// 它只是将其原样传递回给回调函数。
int assignedTimerId = 1;
callback(assignedTimerId, userData); // 调用回调,传入ID和用户数据
printf("定时器库:回调完成。\n");
}
// --- 应用程序代码 ---
// 1. 定义传递给回调函数的数据结构
typedef struct {
const char* message; // 消息字符串
int retryCount; // 重试次数计数器
} MyTimerInfo;
// 2. 实现匹配 TimerCallback 签名的回调函数
void handleTimerExpiration(int timerId, void* userData) {
printf("我的应用程序:收到定时器 ID %d 的回调。\n", timerId);
// 重要:将 void* 指针强制转换回正确的指针类型 (MyTimerInfo*)
MyTimerInfo* info = (MyTimerInfo*)userData;
printf("消息: %s, 重试次数: %d\n", info->message, info->retryCount);
info->retryCount++; // 修改数据
}
// 3. 主逻辑中,创建数据并注册定时器
int main() {
MyTimerInfo myInfo;
myInfo.message = "任务 A 需要处理";
myInfo.retryCount = 0;
printf("我的应用程序:正在注册定时器...\n");
setTimer(1000, handleTimerExpiration, &myInfo); // 传递 myInfo 的地址
printf("我的应用程序:定时器注册调用完成。\n");
// 打印修改后的重试次数,验证回调函数确实修改了它
printf("我的应用程序:当前重试次数 (回调之后): %d\n", myInfo.retryCount);
return 0; // 程序正常退出
}
重要注意事项:
不能直接解引用 : 你不能直接对 void* 使用 * 运算符,因为编译器不知道它指向的数据类型有多大,以及如何解释这些字节。
必须显式转换: 在使用 void* 指向的数据之前,必须将其显式转换(cast)为正确的具体数据类型指针。
类型安全: void* 的使用会绕过编译器的类型检查。如果你将 void* 转换回了错误的类型,会导致未定义行为(Undefined Behavior),通常是程序崩溃或数据损坏。这是 void* 的主要缺点。
指针运算: 不能对 void* 进行指针算术运算(如 ptr++ 或 ptr + 1),因为编译器不知道每个元素的大小。 (GCC 等编译器可能有扩展允许,但不标准且危险)。
C++ 中的替代方案: 在 C++ 中,虽然 void* 仍然可用且在与 C 库交互时必不可少,但对于泛型编程,通常推荐使用模板(templates),它们提供了类型安全。对于类型转换,C++ 提供了更安全的转换运算符,如 static_cast, dynamic_cast, 和 reinterpret_cast。reinterpret_cast<T*>(void_ptr) 常用于 void* 和其他指针类型之间的转换,但同样需要开发者保证类型转换的正确性。
void* 是一个强大的工具,用于实现通用性,但牺牲了类型安全。使用它时,必须非常小心,确保在解引用或操作指针之前,总是将其转换回正确的原始类型。