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 name16; // 通用属性

} 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_sensorsi);

printf("%s Value: %.1f\n", my_sensorsi->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:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/

相关推荐
Scott9999HH2 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
AimerMilet2 小时前
物联网卡流量池是什么意思?企业用划算吗?
物联网
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海3 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
Xxtaoaooo4 小时前
DolphinDB 物联网数据平台全景:一份从架构到落地的实践地图
物联网·系统架构·时序数据库·数据库架构·dolphindb
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
sramdram6 小时前
MCU微控制器OA打印机方案
单片机·嵌入式硬件·mcu·mcu微控制器
DS小龙哥6 小时前
基于物联网设计的室内环境监测系统(从0讲解如何接入涂鸦智能平台)
物联网