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、接受 intdouble 参数的函数。

回调函数是它的经典用法。比如你写一个事件处理框架:

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* 的核心脉络:

void* 通用指针 通用函数参数 qsort / bsearch 自定义通用算法 函数指针与回调 事件驱动 / 异步通知 策略模式 / 插件化 泛型数据结构 动态数组 / 链表 哈希表 / 树 核心:类型擦除 + 函数指针 + 字节操作

18.6 避坑指南

  • 不要对 void* 做算术运算 —— 标准 C 不允许。转成 char* 再算。
  • 函数指针的调用约定要一致 —— 特别是在嵌入式或跨平台代码中,__stdcall__cdecl 混用会崩溃。
  • 回调中的 user_data 要小心生命周期 —— 如果传了栈上变量的地址,回调执行时变量可能已经销毁。
我曾经犯过的错: 写了一个通用链表,用 void* 存数据。结果存的是局部变量的地址,链表还没遍历完,变量就出作用域了。调试了一下午才发现是野指针问题。

void* 用好了,能让你的代码复用性提升一个档次。但用不好,就是段错误的温床。记住一个原则:void* 只负责「传地址」,不负责「管生命周期」。谁分配的内存,谁负责释放。


公众号:蓝海资料掘金营,微信deep3321