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

相关推荐
北冥有一鲲2 小时前
A2A协议与LangChain.js实战:构建微型软件工厂
开发语言·javascript·langchain
Chen不旧2 小时前
java基于reentrantlock/condition/queue实现阻塞队列
java·开发语言·signal·reentrantlock·await·condition
nuo5342022 小时前
Nuo-Math-Compiler
c语言·编辑器
laplace01233 小时前
Part 3:模型调用、记忆管理与工具调用流程(LangChain 1.0)笔记(Markdown)
开发语言·人工智能·笔记·python·langchain·prompt
风送雨3 小时前
八周Python强化计划(七)
开发语言·python
ππ很开心6663 小时前
DAY 32 函数专题2:装饰器
开发语言·python
Knight_AL3 小时前
阿里《Java 开发手册》下的对象构建与赋值规范实践
java·开发语言
lsx2024063 小时前
SQL LIKE 操作符详解
开发语言
DuHz3 小时前
242-267 GHz双基地超外差雷达系统:面向精密太赫兹传感与成像的65nm CMOS实现——论文阅读
论文阅读·物联网·算法·信息与通信·毫米波雷达