C语言如何实现继承?

继承要解决什么问题

先看一个实际场景:项目里有三种I2C传感器,每种都需要I2C读写、设备地址、初始化状态。如果每个传感器都独立定义一遍,I2C相关代码重复三份,改一个地方要改三处。

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

基础做法

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

plaintext 复制代码
typedefstruct {
uint8_t i2c_addr;
uint8_t bus_id;
int initialized;
} i2c_device_t;

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

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

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

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

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

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

基类函数复用

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

plaintext 复制代码
inti2c_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);
}

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

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

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

plaintext 复制代码
intbme280_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;
return0;
}

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

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

向上转型

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

plaintext 复制代码
#define MAX_DEVICES 8

statici2c_device_t *g_devices[MAX_DEVICES];
staticint g_device_count = 0;

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

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

注册时传基类指针:

plaintext 复制代码
staticbme280_t g_bme280;
staticsht30_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 宏从成员指针反推出容器结构体指针:

plaintext 复制代码
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))

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

plaintext 复制代码
voidhandle_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);
} elseif (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挪到了其他位置,直接强转会出bug, container_of 不会。

多层继承

C语言也可以做多层继承,但层数多了代码可读性会下降,一般不超过三层。

plaintext 复制代码
// 第一层:设备基类
typedefstruct {
uint8_t type;
uint8_t id;
int state;
} device_t;

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

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

访问最顶层基类:

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

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

私有成员模拟

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

头文件只声明前向引用:

plaintext 复制代码
// bme280.h
typedefstructbme280_privbme280_priv_t;

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

源文件定义完整结构体:

plaintext 复制代码
// bme280.c
structbme280_priv {
int32_t t_fine;
uint8_t calib[42];
uint8_t oversampling;
};

外部代码只能看到 bme280_priv_t *priv 这个指针,不知道里面有什么,也没法直接访问。这比C++的private弱------别人还是可以通过指针偏移硬读,但至少表达了"别碰这个字段"的意图。

计算机专业,狗都不学?

电子行业未来重要主线:从芯片性能迈向系统整合

Unix时间戳为何从1970年开始?

相关推荐
wengqidaifeng3 小时前
2026 年电赛(TI 杯)H 题:MaxiCamPro、双主控通信、VOFA+ 遥测与可复现实验方法
c语言·单片机·嵌入式硬件
c238565 小时前
C/C++每日一练20
c语言·开发语言·c++
坚持编程的菜鸟5 小时前
模拟实现strncpy
c语言·算法·模拟实现strncpy
CAE二次开发工程师6 小时前
【C语言入门到精通】-基础篇
c语言·开发语言·python
Navigator_Z9 小时前
LeetCode //C - 1187. Make Array Strictly Increasing
c语言·算法·leetcode
别动我齐刘海10 小时前
“三层同步审计”判定掉帧缺失
c语言·c++·人工智能·深度学习·学习·机器学习·机器人
十月的皮皮10 小时前
STM32从零到量产开发:四路继电器工业控制模块开发 -上位机主窗口设计说明
c语言·stm32·单片机·stm32cubemx
wuyk55511 小时前
第1章:无刷电机核心原理与运行机制
c语言·开发语言·stm32·单片机·嵌入式硬件·机器学习
坚持编程的菜鸟11 小时前
模拟实现strncat
c语言·算法·模拟实现strncat