18、void* 的妙用:通用指针、函数指针与回调、实现泛型数据结构
说到 void*,很多初学者会觉得它有点「玄乎」。
嗯,我刚开始学 C 语言时也这么觉得。一个不知道类型的指针,能干嘛?
后来在项目中写底层驱动、做内存池管理,才发现这玩意儿简直是 C 语言的「瑞士军刀」。今天咱们就把它彻底聊透。
18.1 void* 的本质:丢掉类型信息的指针
说白了,void* 就是一个「裸指针」。它只告诉你地址在哪,不告诉你这个地址里存的是什么类型的数据。
int a = 42;
void* p = &a; // 合法,任何指针都能赋给 void*
但你不能直接解引用它:
// printf("%d\n", *p); // 编译错误!不知道类型怎么解引用?
必须强制转换回具体类型才能用:
int* ip = (int*)p;
printf("%d\n", *ip); // 输出 42
我的习惯: 用 void* 做参数传递时,一定在注释里写明「预期类型」。不然三个月后你自己都看不懂。
18.2 通用指针:让函数接受任意类型
标准库里的 qsort 就是典型例子。它怎么做到给任意类型的数组排序?
void qsort(void* base, size_t num, size_t size,
int (*compar)(const void*, const void*));
第一个参数就是 void*,不管你是 int 数组、double 数组还是结构体数组,都能传进去。
我自己写过一个通用的「查找最大值」函数:
void* find_max(void* arr, size_t count, size_t elem_size,
int (*cmp)(const void*, const void*)) {
char* base = (char*)arr; // 按字节操作
void* max = base;
for (size_t i = 1; i < count; i++) {
void* current = base + i * elem_size;
if (cmp(current, max) > 0) {
max = current;
}
}
return max;
}
关键点: 操作 void* 时,通常要转成 char* 才能做指针运算。因为 char* 是 1 字节步长,方便按元素大小偏移。
18.3 函数指针与回调:把行为当作参数传递
函数指针的声明看起来有点吓人:
int (*func_ptr)(int, double);
其实拆开看就简单了:func_ptr 是一个指针,指向一个返回 int、接受 int 和 double 参数的函数。
回调函数是它的经典用法。比如你写一个事件处理框架:
typedef void (*event_handler_t)(int event_id, void* user_data);
void register_handler(event_handler_t handler, void* user_data) {
// 保存 handler 和 user_data
}
void on_button_click(int id, void* data) {
printf("按钮 %d 被点击了\n", id);
}
// 使用时
register_handler(on_button_click, NULL);
我曾经踩过的坑: 函数指针的类型必须严格匹配。参数个数、类型、返回值,差一点都不行。编译器不会帮你做隐式转换。
18.4 实现泛型数据结构:一个简单的动态数组
用 void* 配合函数指针,就能写出「类型无关」的数据结构。下面是一个简化版的动态数组:
typedef struct {
void* data; // 实际数据存储区
size_t elem_size; // 每个元素的大小
size_t capacity; // 容量
size_t size; // 当前元素个数
} DynArray;
DynArray* dynarray_create(size_t elem_size, size_t initial_capacity) {
DynArray* arr = malloc(sizeof(DynArray));
arr->data = malloc(elem_size * initial_capacity);
arr->elem_size = elem_size;
arr->capacity = initial_capacity;
arr->size = 0;
return arr;
}
void dynarray_push(DynArray* arr, void* elem) {
if (arr->size == arr->capacity) {
arr->capacity *= 2;
arr->data = realloc(arr->data, arr->elem_size * arr->capacity);
}
// 按字节拷贝元素
char* dest = (char*)arr->data + arr->size * arr->elem_size;
memcpy(dest, elem, arr->elem_size);
arr->size++;
}
void* dynarray_get(DynArray* arr, size_t index) {
return (char*)arr->data + index * arr->elem_size;
}
用的时候:
DynArray* int_arr = dynarray_create(sizeof(int), 10);
int val = 42;
dynarray_push(int_arr, &val);
int* result = (int*)dynarray_get(int_arr, 0);
printf("%d\n", *result); // 输出 42
注意: 每次 push 都要传元素的地址,而不是值。因为 memcpy 需要知道从哪里拷贝数据。
18.5 知识体系图
下面这张图帮你理清 void* 的核心脉络:
18.6 避坑指南
- 不要对 void* 做算术运算 —— 标准 C 不允许。转成 char* 再算。
- 函数指针的调用约定要一致 —— 特别是在嵌入式或跨平台代码中,
__stdcall和__cdecl混用会崩溃。 - 回调中的 user_data 要小心生命周期 —— 如果传了栈上变量的地址,回调执行时变量可能已经销毁。
我曾经犯过的错: 写了一个通用链表,用 void* 存数据。结果存的是局部变量的地址,链表还没遍历完,变量就出作用域了。调试了一下午才发现是野指针问题。
void* 用好了,能让你的代码复用性提升一个档次。但用不好,就是段错误的温床。记住一个原则:void* 只负责「传地址」,不负责「管生命周期」。谁分配的内存,谁负责释放。