C语言从句柄到对象 (五) —— 虚函数表 (V-Table) 与 RAM 的救赎

在上一篇中,我们通过在结构体里嵌入函数指针,成功实现了 “多态”。 但是,我们留下了一个严重的 RAM 隐患。对于资源极其紧张的单片机(比如只有 2KB RAM 的 Cortex-M0),上一篇的写法可能是“致命”的。

这一篇,我们将深入 C 语言的底层,手动复现 C++ 的核心机制 —— 虚函数表 (V-Table),用“架构设计”来换取宝贵的 RAM 空间。

在上一篇中,我们定义了这样的结构体来实现多态:

struct Sensor_t {
    char name[10];
    float (*read)(struct Sensor_t *self);  // 4字节 RAM
    void  (*init)(struct Sensor_t *self);  // 4字节 RAM
    void  (*reset)(struct Sensor_t *self); // 4字节 RAM
    // ... 假设还有 5 个标准接口
};

这种写法逻辑很完美,但 工程落地很糟糕


一、 算算这笔 RAM 账

假设你正在做一个智能大棚项目,需要部署 100 个温湿度传感器。

如果你使用上面的结构体:

  1. RAM 消耗:每个对象里存了 8 个函数指针。

    100 个对象 x 8个指针 x 4 字节 = 3200 Bytes

  2. 痛点:对于一个 STM32F030(4KB RAM)或者 8051 来说,光存这些指针,内存就爆了,连栈都开不出来。

  3. 浪费:最讽刺的是,对于这 100 个同类型的传感器(比如都是 DHT11),这 800 个函数指针的值是 完全一样 的!它们都指向 DHT11_Read, DHT11_Init...

既然是一样的,为什么要重复存 100 遍?


二、 解决方案:提炼“虚表” (The V-Table)

我们需要把这些 “不变的函数指针” 剥离出来,放到一个单独的表中。

因为这些表在编译后就不会变了,我们可以把它加上 const 修饰符,强制链接器把它放到 Flash (RO-Data) 里,而不占用宝贵的 RAM。

这个表,在 C++ 里叫 虚函数表 (Virtual Function Table, vtable);在 Linux 内核驱动里叫 操作集 (Operations, ops)

2.1 第一步:定义接口表 (The Ops Struct)

我们把所有的函数指针拿出来,单独定义一个结构体。

// sensor_ops.h

// 前向声明
struct Sensor_t;

// 定义操作集 (V-Table)
typedef struct {
    // 这一组函数指针,代表了“Sensor”这个类的标准行为
    void  (*init)(struct Sensor_t *self);
    float (*read)(struct Sensor_t *self);
    void  (*reset)(struct Sensor_t *self);
} Sensor_Ops_t;

2.2 第二步:改造对象 (The Object)

现在的 Sensor_t 对象里,不再存储那一堆函数指针了,而是只存 一个指针,指向那个表。

// sensor.h

struct Sensor_t {
    char name[10]; // 对象的属性(每个对象不同)
    
    // 【核心变化】
    // 只存一个指向 Ops 表的指针!
    // 无论 Ops 里有多少个函数,这里永远只占 4 字节。
    const Sensor_Ops_t *ops; 
    
    void *private_data; // 私有数据
};

三、 实例化:Flash 与 RAM 的完美分离

现在我们来看看,在 main.c 或驱动文件里怎么写。

3.1 定义具体的驱动表 (In Flash)

我们在驱动文件(如 dht11.c)里,定义一个 static const 的表。

// dht11_driver.c

// 具体函数的实现
static float DHT11_Read(struct Sensor_t *self) { ... }
static void  DHT11_Init(struct Sensor_t *self) { ... }
static void  DHT11_Reset(struct Sensor_t *self) { ... }

// 【关键优化】
// 加了 const,这块数据会被直接烧录到 Flash 中
// 运行时完全不占用 RAM
static const Sensor_Ops_t dht11_ops = {
    .init  = DHT11_Init,
    .read  = DHT11_Read,
    .reset = DHT11_Reset,
};

// 对外只暴露这个 Ops 表的地址,或者提供一个绑定函数
const Sensor_Ops_t* Get_DHT11_Ops(void) {
    return &dht11_ops;
}

3.2 初始化对象 (In RAM)

// main.c

int main() {
    // 实例化 100 个对象
    struct Sensor_t sensors[100];
    
    // 初始化第一个
    sensors[0].ops = Get_DHT11_Ops(); // 指针指向 Flash 里的表
    
    // 初始化第二个
    sensors[1].ops = Get_DHT11_Ops(); // 指向同一个 Flash 地址
    
    // ...
}

四、 调用的变化:多了一层“跳板”

使用 V-Table 后,调用的语法会稍微变繁琐一点点(这就是 C++ 帮我们隐藏掉的细节)。

之前的调用: s->read(s);

现在的调用: s->ops->read(s);

我们可以写一个 内联函数 (Inline Wrapper) 来让调用变优雅:

// sensor.h
static inline float Sensor_Read(struct Sensor_t *s) {
    // 安全检查:防止空指针
    if (s && s->ops && s->ops->read) {
        return s->ops->read(s);
    }
    return 0.0f;
}

// main.c
val = Sensor_Read(&sensors[0]); // 看起来和普通函数没区别了!

五、 效果对比:降维打击

让我们重新算一下那笔账。假设有 100 个对象,每个对象包含 8 个接口函数。

方案方案 A:函数指针在对象内方案 B:虚函数表 (V-Table)
RAM 占用100 × 8 × 4 = 3200 Bytes100 × 1 × 4 = 400 Bytes
Flash 占用0 (代码逻辑除外)1 × 8 × 4 = 32 Bytes (存那个表)
初始化速度慢 (要赋值 800 次指针)极快 (只赋值 100 次指针)
可维护性差 (容易漏赋值某个函数) (编译期检查结构体初始化)

结论: 通过引入 Ops 结构体,我们用微不足道的 Flash 空间(32字节),换回了巨量的 RAM 空间(2800字节)。 在嵌入式开发中,Flash 往往是富余的,而 RAM 永远是紧缺的。 这种交易简直是一本万利。


六、 进阶伏笔:父类如何访问子类?

到目前为止,我们的 Sensor_t 看起来很完美。 但在写 DHT11_Read 的具体实现时,你会发现一个巨大的问题:

static float DHT11_Read(struct Sensor_t *self) {
    // 问题来了:
    // self 是一个通用的 Sensor_t 指针。
    // 但是 DHT11 驱动需要知道具体的 GPIO 引脚号(Pin)。
    // Pin 存在哪里?Sensor_t 里没有 Pin 变量啊!
    
    // 我们上一期预留了一个 void *private_data,可以用它:
    dht11_config_t *cfg = (dht11_config_t *)self->private_data;
    HAL_GPIO_ReadPin(cfg->port, cfg->pin); // 可以工作,但不够优雅
}

void* 强转虽然可行,但它是 “弱类型” 的,不安全。 而且,如果我想实现 “继承”(比如 DHT11 继承自 Sensor),让 DHT11 结构体直接 包含 Sensor 的所有属性,该怎么做?

Linux 内核里大量使用的 container_of 宏和“结构体嵌套”技术,正是为了解决这个问题。

/*******************************************
* Description:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值