嵌入式开发中 void 的用法总结

嵌入式开发中 void 的用法总结

在 C 语言里,void 是一个特殊的关键字——它代表"无"或"任意"。对于嵌入式开发者来说,真正需要掌握的核心是 void*(万能指针),它是 C 语言实现"泛型"和"多态"的唯一手段,在 RTOS、驱动抽象、硬件寄存器操作中无处不在。

本文将从基础到实战,系统总结 void 在嵌入式开发中的所有用法。


一、void 作为函数返回值 / 参数

最基础的用法:表示"没有"

void led_on(void);  // 无参数、无返回值
  • void 作返回值 → 函数不返回任何东西
  • void 作参数 → 函数明确不接受任何参数(现代 C 标准推荐写法)

注意: void func()void func(void) 在 C 中含义不同。前者表示"参数未指定",后者才是"明确无参数"。嵌入式开发中推荐始终使用 void func(void)


二、void* 万能指针 — 最核心的用法

int *     → 只能指向 int
uint8_t * → 只能指向字节
void *    → 什么都能指(使用时需要强制转换)

void* = 万能地址,只保存内存地址,不关心指向的数据类型,使用时再强制转换为具体类型。

核心作用:能指向任何数据类型,常见于以下场景:

  • 内存分配返回(malloc / 自定义内存池)
  • 通用回调的"用户数据"参数
  • 通用链表/队列/容器(data 字段用 void*
  • 寄存器/硬件抽象层传递缓冲区

经典例子一:内存分配(malloc 返回 void*)

mallocvoid* 最经典的使用场景——它不知道你要分配什么类型的内存,所以返回 void*,由你自己强转:

// malloc 原型:
void *malloc(size_t size);  // 返回 void*,你要什么类型自己转

// 标准用法
int *arr = (int *)malloc(10 * sizeof(int));     // 分配 10 个 int
char *buf = (char *)malloc(128);                // 分配 128 字节缓冲区

在嵌入式中,很多 RTOS 提供自己的内存分配函数,原理一样:

// FreeRTOS 的内存分配,同样返回 void*
void *pvPortMalloc(size_t xWantedSize);

// 自定义内存池(嵌入式常见做法,避免碎片)
static uint8_t pool[4096];           // 预分配的内存池
static size_t pool_offset = 0;

void *my_malloc(size_t size) {
    void *ptr = &pool[pool_offset];  // 返回 void*
    pool_offset += size;
    return ptr;
}

// 使用时强转为具体类型
typedef struct { uint8_t id; uint16_t value; } SensorData;
SensorData *data = (SensorData *)my_malloc(sizeof(SensorData));
data->id = 1;
data->value = 1024;

核心思路: 内存分配函数只管"给你一块内存",不关心你拿来存什么,所以用 void* 返回。

经典例子二:回调 + void* 传参

同一个 commonPrint 框架,通过传入不同的函数指针和参数,实现打印不同类型的数据:

#include <stdio.h>

// void* 万能指针
void printIntger(void *a)
{
    int *pointer, value;
    pointer = (int *)a;  // 强制转换为 int* 指针
    value = *pointer;
    printf("打印整型值:%d\r\n", value);
}

// 再写一个回调函数,打印 float
void printFloat(void *a)
{
    float *p = (float *)a;
    printf("打印浮点值:%f\r\n", *p);
}

// 函数指针 callback(同时传入函数指针 + 函数的形参)
// func: 回调函数指针
// arg:  回调函数需要的参数,用 void* 万能指针传递
void commonPrint(void (*func)(void *), void *arg)
{
    func(arg);  // 把外部传入的参数转发给回调函数
}

int main(void)
{
    int intger = 100;
    float fval = 3.14f;

    // 直接调用
    printIntger(&intger);

    // 通过 commonPrint 传入函数指针 + 参数
    // 同一个 commonPrint,传不同的回调和参数
    commonPrint(printIntger, &intger);  // 打印 int
    commonPrint(printFloat, &fval);     // 打印 float
}

核心套路:函数指针决定"做什么",void* 决定"用什么数据做"。 commonPrint 本身不需要知道数据是什么类型,全靠调用者决定。
image-20260308161352227

经典例子三:pthread_create

// 原型:
int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *),  // 线程入口函数
                   void *arg);                       // 传给线程的参数

用法:

typedef struct {
    int id;
    char name[32];
} Task;

void *worker(void *arg) {
    Task *task = (Task *)arg;  // 强转回具体类型
    printf("任务 %d: %s\n", task->id, task->name);
    return NULL;
}

Task t = {1, "LED闪烁"};
pthread_create(&tid, NULL, worker, &t);
//                          ↑回调    ↑参数(void*传任意数据)

套路是一样的: 框架(pthread)不知道你要传什么数据,所以用 void* 接收,你在回调里自己强转回来。


三、嵌入式实战场景

场景一:硬件寄存器操作(void* 当内存地址用)

// 直接读写寄存器地址
#define GPIO_BASE  ((void *)0x40020000)

volatile uint32_t *gpio = (volatile uint32_t *)GPIO_BASE;
*gpio |= (1 << 5);  // 置位第5个引脚

场景二:通用消息队列(RTOS 常用)

// FreeRTOS 的队列可以传任意类型的数据
// 内部就是用 void* + memcpy 按字节搬运
typedef struct {
    uint8_t cmd;
    uint16_t value;
} Msg;

Msg msg = {0x01, 1024};
// BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
xQueueSend(queue, &msg, portMAX_DELAY);  // 传入 void*

场景三:统一的驱动接口(回调 + void*)

// 定义一个通用的传感器驱动接口
typedef struct {
    void (*init)(void *config);       // 初始化
    int  (*read)(void *config);       // 读数据
    void (*deinit)(void *config);     // 反初始化
} SensorDriver;

// 温度传感器的实现
typedef struct { uint8_t i2c_addr; } TempConfig;

void temp_init(void *config) {
    TempConfig *cfg = (TempConfig *)config;
    i2c_init(cfg->i2c_addr);
}

int temp_read(void *config) {
    TempConfig *cfg = (TempConfig *)config;
    return i2c_read(cfg->i2c_addr);
}

// 注册驱动
SensorDriver temp_drv = { temp_init, temp_read, NULL };
TempConfig   temp_cfg = { 0x48 };

// 统一调用,不管是什么传感器
temp_drv.init(&temp_cfg);
int val = temp_drv.read(&temp_cfg);

这就是经典的策略模式——函数指针决定"做什么",void* 决定"用什么数据做"


四、总结速查表

用法 含义 嵌入式典型场景
void 无返回值 函数不返回数据 中断处理函数 void EXTI0_IRQHandler(void)
void 无参数 函数不接受参数 void system_init(void)
void* 万能指针 可指向任意类型 mallocpvPortMalloc、自定义内存池
void* + 回调 通用接口传递数据 RTOS 线程、驱动抽象、事件分发
void* + 强转 运行时类型还原 同一套接口处理不同类型数据

一句话总结:void* 是 C 语言实现"泛型"和"多态"的唯一手段,嵌入式开发里到处都是。掌握 void* + 函数指针的组合,就掌握了 C 语言面向对象编程的核心。