C语言从句柄到对象 (六) —— 继承与 HAL:父类指针访问子类数据

我们终于走到了这里。 从最开始的“全局变量满天飞”,到“句柄封装”,再到“多态虚表”,我们的 C 代码已经有了 C++ 的 90% 的功力。

今天,我们要攻克面向对象三大支柱的最后一根——继承 (Inheritance)。 我们将揭示 Linux 内核中无处不在的“黑魔法”:如何在只有父类指针的情况下,安全地访问子类的私有数据?

这篇写完,你就可以自信地说:我会写 HAL(硬件抽象层) 了。

上一期中,我们利用“虚函数表”解决了逻辑复用的问题。 但是,在实现具体的驱动函数时,我们遇到了一个数据访问的难题。

场景回顾: 我们的系统里有一个通用的父类 Sensor_t

struct Sensor_t {
    const Sensor_Ops_t *ops; // 虚表指针
    // void *private_data;   // 上一期用的笨办法:万能指针
};

我们需要实现一个子类 DHT11,它需要一个私有的变量 uint16_t gpio_pin

如果使用 void *private_data,虽然能解决问题,但有两个缺点:

  1. 浪费内存:多存一个指针(4字节)。

  2. 不安全void* 是类型不安全的,全靠程序员自觉转换,转错了编译器也不报错。

今天我们介绍 C 语言实现继承的标准做法:结构体嵌套与首地址原则


一、 内存布局的秘密:结构体嵌套

在 C++ 中,继承是编译器帮你做的。在 C 语言中,我们要手工做。 所谓的“继承”,本质上就是 “子类包含了父类的所有内容”

1.1 定义子类 (Derived Class)

我们不再使用 void* 挂载数据,而是直接把父类结构体 嵌入 到子类结构体中。 【关键规则】:父类必须放在子类的第一个成员位置!

// sensor.h (父类)
typedef struct {
    const char *name;
    const Sensor_Ops_t *ops;
} Sensor_t;

// dht11.h (子类)
typedef struct {
    // 【继承的核心】父类必须是第一个成员
    Sensor_t  parent; 
    
    // 子类特有的私有数据
    GPIO_TypeDef *port;
    uint16_t      pin;
} DHT11_t;

1.2 内存里的样子

为什么父类一定要放在第一个? 因为 C 语言标准规定:结构体的地址,等于它第一个成员的地址。

这意味着: 如果我们有一个 DHT11_t 类型的变量 dht11_obj

  • &dht11_obj 的地址(子类地址)

  • &dht11_obj.parent 的地址(父类地址)

它们在数值上是完全相等的!


二、 向下转型 (Downcasting):父类变子类

有了这个内存布局特性,我们就可以施展“黑魔法”了。 只要给我一个父类指针 Sensor_t*,我就能直接把它强转为子类指针 DHT11_t*,从而访问子类的私有数据!

2.1 驱动实现的进化

让我们看看驱动代码 dht11.c 如何利用这一特性。

// dht11.c

// 具体的读取函数
// 注意:接口定义的参数依然是父类指针 (Sensor_t*)
static float DHT11_Read(Sensor_t *base) {
    // 【黑魔法时刻】向下转型 (Downcasting)
    // 因为 parent 是 DHT11_t 的第一个成员,所以 base 的地址就是 DHT11_t 的地址
    // 我们直接强制转换!
    DHT11_t *self = (DHT11_t *)base;
    
    // 现在,我们可以通过 self 访问子类特有的数据了
    // 编译器完全能看懂 self->port 和 self->pin
    HAL_GPIO_ReadPin(self->port, self->pin);
    
    return 25.0f; // 假装读到了数据
}

// 虚表定义
static const Sensor_Ops_t dht11_ops = {
    .read = DHT11_Read, // 绑定函数
};

不需要 void*,不需要 malloc 额外的私有数据结构。 一个指针,两套身份。 对外是通用的 Sensor,对内是具体的 DHT11


三、 完整实战:手写 Mini-HAL

让我们把这 6 期学到的所有东西(句柄、多态、虚表、继承)串起来,写一个完整的 Mini-HAL (Hardware Abstraction Layer)

3.1 步骤一:定义父类与虚表 (HAL Interface)

/* sensor_hal.h */
struct Sensor_t; // 前向声明

// 1. 虚表 (V-Table)
typedef struct {
    void  (*init)(struct Sensor_t *base);
    float (*read)(struct Sensor_t *base);
} Sensor_Ops_t;

// 2. 父类 (Base Class)
typedef struct Sensor_t {
    const Sensor_Ops_t *ops; // 多态的核心
    char name[16];           // 通用属性
} Sensor_t;

// 3. 多态调用接口 (Wrapper)
static inline float Sensor_Read(Sensor_t *s) {
    return s->ops->read(s);
}

3.2 步骤二:定义子类 (Concrete Driver)

/* dht11.c */
typedef struct {
    Sensor_t parent; // 【继承】必须在第一位
    uint16_t pin;    // 【私有】
} DHT11_t;

// 具体实现
static float DHT11_Read_Imp(Sensor_t *base) {
    // 【转型】父类指针 -> 子类指针
    DHT11_t *self = (DHT11_t *)base;
    
    printf("Reading DHT11 on Pin %d\n", self->pin);
    return 26.5f; 
}

// 虚表实例化 (存 Flash)
static const Sensor_Ops_t dht11_ops = {
    .read = DHT11_Read_Imp,
};

// 初始化函数 (构造函数)
void DHT11_Init(DHT11_t *self, const char *name, uint16_t pin) {
    // 1. 初始化父类
    self->parent.ops = &dht11_ops; // 挂载虚表
    strcpy(self->parent.name, name);
    
    // 2. 初始化子类
    self->pin = pin;
}

步3.3 骤三:业务层调用 (Application)

/* main.c */
int main() {
    // 1. 静态分配内存 (在栈上或全局区,无 malloc)
    DHT11_t dht_living; 
    DHT11_t dht_bed;
    
    // 2. 初始化 (构造)
    DHT11_Init(&dht_living, "LivingRoom", 5);
    DHT11_Init(&dht_bed,    "BedRoom",    12);
    
    // 3. 放入通用数组 (向上转型 Upcasting)
    // 这里的数组类型是父类指针!
    Sensor_t *my_sensors[] = {
        (Sensor_t *)&dht_living,
        (Sensor_t *)&dht_bed,
    };
    
    // 4. 统一调用 (Polymorphism)
    for (int i = 0; i < 2; i++) {
        // App 层完全不知道 DHT11 的存在,只认识 Sensor_t
        float val = Sensor_Read(my_sensors[i]);
        printf("[%s] Value: %.1f\n", my_sensors[i]->name, val);
    }
}

四、 进阶:如果父类不在第一位怎么办?

有些高阶场景(比如多重继承,或者使用了链表节点),父类结构体可能无法放在子类的第一位。

struct DHT11_t {
    uint16_t pin;      
    Sensor_t parent; // 放在了中间!
};

此时 (DHT11_t*)base 这种简单粗暴的强转就会出错,指针会指偏。 这时候就需要请出 Linux 内核中最著名的宏:container_of

#define container_of(ptr, type, member) ({ \
    const typeof(((type *)0)->member) *__mptr = (ptr); \
    (type *)((char *)__mptr - offsetof(type, member)); })

原理:它通过计算 parent 成员在 DHT11_t 结构体中的 偏移量 (Offset),反向推算出结构体的首地址。

用法

static float DHT11_Read_Imp(Sensor_t *base) {
    // 无论 parent 在哪,都能找回来
    DHT11_t *self = container_of(base, DHT11_t, parent);
    // ...
}

注:在大多数简单的嵌入式 HAL 设计中,遵守“首地址原则”足够了,container_of 属于屠龙技。

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

 

父类访问子类对象时,要调用子类特有的方法,通常可以采用类型转换的方式。在大多数面向对象的编程语言中,使用`cast`操作(在不同语言中可能有不同的具体语法)将父类转换为子类,从而实现调用子类特有方法的目的。不过,这种转换可能会失败,所以在转换前最好进行类型检查。 以下以 Java 语言为例,展示如何进行类型转换并调用子类特有的方法: ```java class Parent { public void commonMethod() { System.out.println("This is a common method in Parent class."); } } class Child extends Parent { public void commonMethod() { System.out.println("This is the overridden common method in Child class."); } public void specialMethod() { System.out.println("This is a special method in Child class."); } } public class Main { public static void main(String[] args) { Parent parentHandle = new Child(); // 父类指向子类对象 if (parentHandle instanceof Child) { Child childHandle = (Child) parentHandle; // 类型转换 childHandle.specialMethod(); // 调用子类特有的方法 } } } ``` 在上述 Java 代码中,首先定义了`Parent`类和`Child`类,`Child`类继承自`Parent`类。在`main`方法里,父类`parentHandle`指向了子类对象。通过`instanceof`关键字检查`parentHandle`是否指向一个`Child`对象,如果是,则将其强制转换为`Child`类的句`childHandle`,进而调用`Child`类特有的`specialMethod`方法。 在 SystemVerilog 中,可以使用`$cast`系统任务来实现类似的功能,示例代码如下: ```systemverilog class Parent; virtual function void commonMethod(); $display("This is a common method in Parent class."); endfunction endclass class Child extends Parent; function void commonMethod(); $display("This is the overridden common method in Child class."); endfunction function void specialMethod(); $display("This is a special method in Child class."); endfunction endclass module test; Parent parentHandle; Child childHandle; initial begin childHandle = new(); parentHandle = childHandle; // 父类指向子类对象 if ($cast(childHandle, parentHandle)) begin childHandle.specialMethod(); // 调用子类特有的方法 end end endmodule ``` 在这段 SystemVerilog 代码中,定义了`Parent`类和`Child`类,`Child`类继承自`Parent`类。在`initial`块中,将子类赋值给父类,然后使用`$cast`系统任务尝试将父类转换为子类,如果转换成功,则调用子类特有的`specialMethod`方法[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值