C语言从句柄到对象 (四) —— 接口抽象:从 Switch-Case 到通用接口

前言: 在前三期(句柄篇、封装篇、内存篇)中,我们通过引入“句柄”和“不透明指针”,成功地把数据封装进了黑盒子里。 今天,我们面临一个新的挑战:如何把**逻辑(函数)**也封装进对象里?

这不仅仅是代码风格的改变,这是嵌入式**“硬件抽象层 (HAL)”** 设计的核心思想。掌握了它,你就能写出那类 “换了芯片,业务代码一行都不用改” 的高内聚代码。


一、 痛点:被 Switch-Case 支配的恐惧

假设我们正在开发一个智能家居网关,产品经理要求支持三种不同的传感器:

  1. DHT11 (单总线温湿度)

  2. SHT30 (I2C 温湿度)

  3. ADC (光敏电阻电压)

如果你还停留在“面向过程”的思维里,你的读取函数大概率会写成这样:

// 定义传感器类型
typedef enum { SENSOR_DHT11, SENSOR_SHT30, SENSOR_ADC } SensorType;

// 传感器句柄
typedef struct {
    SensorType type;
    // ... 其他私有数据 ...
} Sensor_t;

// 统一读取函数
float Sensor_Read(Sensor_t *s) {
    // 典型的“面向过程”:调用者必须知道所有底层的细节
    if (s->type == SENSOR_DHT11) {
        return DHT11_Read_OneWire(); // 调底层 A
    } 
    else if (s->type == SENSOR_SHT30) {
        return SHT30_Read_I2C();     // 调底层 B
    } 
    else if (s->type == SENSOR_ADC) {
        return ADC_Get_Voltage();    // 调底层 C
    }
    return 0.0f;
}

这种写法有两个致命问题:

  1. 耦合度极高Sensor_Read 这个上层业务函数,竟然需要包含所有底层驱动的头文件!如果底层驱动有几十个,头文件引用就会极其混乱。

  2. 违反“开闭原则”:如果你明天买了个新传感器(比如 BME280),你必须去修改 Sensor_Read 函数,加一个 else if改代码就有风险,这就叫“由于扩展功能而导致旧功能崩溃”。


二、 救星:把函数变成“变量”

怎么解决? 我们要让 Sensor_t 这个结构体自己 “知道” 该怎么读取数据,而不是让外面的函数去判断。

C 语言允许我们定义 函数指针。我们可以把“读取动作”存到结构体里,让对象自带方法。

2.1 定义带“动作”的结构体

// sensor.h

// 1. 前向声明
struct Sensor_t;

// 2. 定义接口原型
// 注意:第一个参数通常是对象指针自己 (this 指针)
// 这样底层驱动才能知道自己在操作哪个设备
typedef float (*Sensor_Read_Ops)(struct Sensor_t *self); 

// 3. 定义类
struct Sensor_t {
    char name[10];
    
    // 【关键点】这个变量存的不是数,而是函数的地址!
    Sensor_Read_Ops read; 
    
    // 私有数据指针 (配合上一期的不透明指针使用)
    void *private_data; 
};

2.2 具体化实例 (Instantiate)

现在,我们在初始化各种传感器时,把对应的底层函数填进去。

// main.c

// 底层驱动函数 A (符合 Sensor_Read_Ops 签名)
float DHT11_Driver(struct Sensor_t *self) { 
    printf("Reading DHT11 for %s...\n", self->name); 
    return 25.5f; 
}

// 底层驱动函数 B
float SHT30_Driver(struct Sensor_t *self) { 
    printf("Reading SHT30 via I2C for %s...\n", self->name); 
    return 26.0f; 
}

int main() {
    // 创建传感器对象:在初始化时绑定具体的“驱动函数”
    struct Sensor_t s1 = { .name = "LivingRoom", .read = DHT11_Driver }; 
    struct Sensor_t s2 = { .name = "BedRoom",    .read = SHT30_Driver }; 
    
    // ...
}

三、 见证奇迹:多态调用

现在,我们的上层业务逻辑(App层)发生了翻天覆地的变化:

// 业务层代码
void App_Task_Monitor(struct Sensor_t *s) {
    // 【核心变化】
    // 1. 我不需要判断 s 是什么类型
    // 2. 我也不需要写 switch-case
    // 3. 我只管调用 s->read(),它自己知道该跑哪个驱动!
    
    float val = s->read(s); 
    
    printf("Sensor [%s] Value: %.1f\n", s->name, val);
}

int main() {
    // 定义对象
    struct Sensor_t s1 = { "S1", DHT11_Driver };
    struct Sensor_t s2 = { "S2", SHT30_Driver };
    
    // 统一调用
    App_Task_Monitor(&s1); // 自动调用 DHT11 代码
    App_Task_Monitor(&s2); // 自动调用 SHT30 代码
}

这样做的好处是什么?

  1. 完全解耦App_Task_Monitor 再也不需要引用 dht11.hsht30.h 了。它只认识标准的 Sensor_t 接口。

  2. 极致扩展:如果明天来了个 BME280,你只需要定义一个新的 struct Sensor_t s3 = {..., BME280_Driver}App_Task 函数一行代码都不用改!

这就是 多态 (Polymorphism) —— 同一个接口 (s->read()),在不同对象上表现出不同的行为。


四、 现实中的尴尬:RAM 的隐忧

细心的嵌入式工程师可能已经发现了一个隐患。

上面的写法在对象很少时没问题。但如果你的系统比较复杂,Sensor_t 结构体里不止一个函数,而是有一组函数(接口表):

struct Sensor_t {
    // ... 数据 ...
    void (*init)(void);
    float (*read)(void);
    void (*write)(float);
    void (*sleep)(void);
    void (*reset)(void);
};

如果你有 100 个传感器对象:

  • RAM 消耗 = 100 个对象 × 5 个指针 × 4 字节 = 2000 字节!

  • 关键是:对于所有的 DHT11 对象来说,这 5 个函数指针的值是完全一样的(都指向 DHT11_xxx 函数)。我们为什么要重复存 100 遍一模一样的数据?

怎么优化? 能不能把这组函数指针提取出来,存到 Flash 里(只存一份),每个对象只存一个指向 Flash 的“索引指针”?

这就是 C++ 编译器在幕后为你做的 “虚函数表 (V-Table)” 技术。 下一期,我们将手动实现它,让你的 C 代码既具备面向对象的灵活性,又拥有嵌入式级别的 RAM 优化。

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值