我们终于走到了这里。 从最开始的“全局变量满天飞”,到“句柄封装”,再到“多态虚表”,我们的 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,虽然能解决问题,但有两个缺点:
-
浪费内存:多存一个指针(4字节)。
-
不安全:
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:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/
652

被折叠的 条评论
为什么被折叠?



