找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4789

积分

0

好友

623

主题
发表于 昨天 01:28 | 查看: 11| 回复: 0

继承要解决什么问题

先看一个实际场景:项目里接了三颗 I2C 传感器,每颗都要重新实现 I2C 读写、设备地址和初始化状态。同样的逻辑复制三份,改一个地方就得同步三处,维护成本不低。

继承的本质,就是把共性抽到基类,个性留在派生类。

基础做法

C语言实现继承的核心手段就一个:把基类结构体作为派生类的第一个成员。

typedef struct {
    uint8_t i2c_addr;
    uint8_t bus_id;
    int     initialized;
} i2c_device_t;

typedef struct {
    i2c_device_t base;    // 基类,必须放第一个
    int32_t t_fine;       // BME280特有
    uint8_t calib[42];    // BME280特有
} bme280_t;

typedef struct {
    i2c_device_t base;    // 基类,必须放第一个
    uint8_t crc_enabled;  // SHT30特有
    uint8_t repeatability;
} sht30_t;

为什么必须放第一个?C 标准(C99 6.7.2.1)保证:结构体首成员的偏移量为 0,首成员地址等于结构体本身地址。这意味着:

bme280_t bme;
i2c_device_t *dev = (i2c_device_t *)&bme;  // 安全,地址相同
i2c_device_t *dev2 = &bme.base;             // 等价写法

两种写法指向同一个地址。这也是 C 语言实现“向上转型”的基础——把派生类指针当基类指针用,不需要额外偏移计算。

如果 base 不是第一个成员,强转后地址就错了,读到的字段全部错乱,在 MCU 上大概率直接 HardFault。

基类函数复用

基类定义共性接口,派生类直接调用:

int i2c_device_init(i2c_device_t *dev, uint8_t bus_id, uint8_t addr)
{
    dev->i2c_addr = addr;
    dev->bus_id = bus_id;
    dev->initialized = 1;
    return i2c_bus_check(bus_id);
}

int i2c_device_read_reg(i2c_device_t *dev, uint8_t reg,
                        uint8_t *buf, uint16_t len)
{
    return i2c_transfer(dev->bus_id, dev->i2c_addr, reg, buf, len);
}

int i2c_device_write_reg(i2c_device_t *dev, uint8_t reg,
                         const uint8_t *buf, uint16_t len)
{
    return i2c_transfer_write(dev->bus_id, dev->i2c_addr, reg, buf, len);
}

派生类构造时先调基类构造函数,类似 C++ 的初始化列表:

int bme280_init(bme280_t *dev, uint8_t bus_id, uint8_t addr)
{
    // 先初始化基类
    int ret = i2c_device_init(&dev->base, bus_id, addr);
    if (ret != 0) return ret;

    // 再初始化派生类自有属性
    ret = i2c_device_read_reg(&dev->base, 0x88, dev->calib, 24);
    if (ret != 0) return ret;

    ret = i2c_device_read_reg(&dev->base, 0xA1, dev->calib + 24, 1);
    if (ret != 0) return ret;

    ret = i2c_device_read_reg(&dev->base, 0xE1, dev->calib + 25, 7);
    if (ret != 0) return ret;

    dev->t_fine = 0;
    return 0;
}

读取传感器数据时,基类函数直接复用:

int bme280_read_raw(bme280_t *dev, uint8_t *raw_data)
{
    // 复用基类的i2c读函数
    return i2c_device_read_reg(&dev->base, 0xF7, raw_data, 8);
}

向上转型

有了结构体嵌套,就可以用一个基类指针数组管理所有派生类对象:

#define MAX_DEVICES 8

static i2c_device_t *g_devices[MAX_DEVICES];
static int g_device_count = 0;

void device_register(i2c_device_t *dev)
{
    if (g_device_count < MAX_DEVICES) {
        g_devices[g_device_count++] = dev;
    }
}

void devices_check_all(void)
{
    for (int i = 0; i < g_device_count; i++) {
        if (!g_devices[i]->initialized) {
            printf("Device 0x%02X not initialized\n", g_devices[i]->i2c_addr);
        }
    }
}

注册时传基类指针:

static bme280_t g_bme280;
static sht30_t  g_sht30;

bme280_init(&g_bme280, 0, 0x76);
sht30_init(&g_sht30, 0, 0x44);

device_register(&g_bme280.base);  // 向上转型
device_register(&g_sht30.base);   // 向上转型

device_register 只知道 i2c_device_t,不关心具体是 BME280 还是 SHT30。后续新增传感器类型,注册逻辑完全不用动。

向下转型

向上转型是安全的,因为地址相同;向下转型则需要额外信息——你必须知道这个基类指针实际指向哪个派生类。

Linux 内核container_of 宏从成员指针反推出容器结构体指针:

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

原理不复杂:已知成员指针和它在结构体中的偏移量,用指针减去偏移量,就得到结构体首地址。

void handle_device_event(i2c_device_t *dev)
{
    // 假设通过地址判断具体类型
    if (dev->i2c_addr == 0x76) {
        bme280_t *bme = container_of(dev, bme280_t, base);
        float temp, humi;
        bme280_read_compensated(bme, &temp, &humi);
    } else if (dev->i2c_addr == 0x44) {
        sht30_t *sht = container_of(dev, sht30_t, base);
        float temp, humi;
        sht30_read(sht, &temp, &humi);
    }
}

因为 base 是第一个成员,offsetof(bme280_t, base) 等于 0,所以 container_of 的结果和直接强转 (bme280_t *)dev 一样。但 container_of 更安全——假如有人把 base 挪到其他位置,直接强转会埋雷,container_of 不会。

多层继承

C 语言同样能做多层继承,不过层数一多,可读性会明显下降,一般不建议超过三层。

// 第一层:设备基类
typedef struct {
    uint8_t type;
    uint8_t id;
    int     state;
} device_t;

// 第二层:I2C设备,继承device_t
typedef struct {
    device_t base;       // 继承device_t
    uint8_t  i2c_addr;
    uint8_t  bus_id;
} i2c_device_t;

// 第三层:BME280,继承i2c_device_t
typedef struct {
    i2c_device_t base;   // 继承i2c_device_t
    int32_t t_fine;
    uint8_t calib[42];
} bme280_t;

访问最顶层基类:

bme280_t bme;
device_t *dev = (device_t *)&bme;  // 仍然安全,地址相同

多层继承的代价是访问中间层成员时路径会变长:bme.base.base.id。如果觉得太啰嗦,可以用宏或内联函数封装。

私有成员模拟

C 语言没有 private 关键字,但可以用不透明指针(opaque pointer)模拟私有成员。

头文件只声明前向引用:

// bme280.h
typedef struct bme280_priv bme280_priv_t;

typedef struct {
    i2c_device_t base;
    bme280_priv_t *priv;  // 私有数据,外部看不到内容
} bme280_t;

源文件定义完整结构体:

// bme280.c
struct bme280_priv {
    int32_t t_fine;
    uint8_t calib[42];
    uint8_t oversampling;
};

外部代码只能看到 bme280_priv_t *priv 这个指针,既不知道里面装了什么,也没法直接访问。这比 C++ 的 private 弱一些——别人仍可通过指针偏移硬读,但至少清晰表达了“别碰这个字段”的意图。

这类 C 语言模拟继承的实践在嵌入式项目里很常见,尤其是共享 I2C/SPI 总线、做传感器抽象的时候。如果你也在踩这些坑,欢迎到 云栈社区 一起交流。




上一篇:嵌入式ISR编写规范:可重入性、临界区保护与嵌套避坑
下一篇:C 语言位域与位操作:从 MCU 寄存器到协议打包的精细控制
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-3 21:32 , Processed in 0.801813 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表