07 结构体嵌套:公共数据与行为的复用
07 结构体嵌套:公共数据与行为的复用
C 语言面向对象系列
前面的封装让每个 LED 拥有独立数据。接下来考虑多种 LED 实现:GPIO LED、PWM LED、呼吸灯和 RGB LED 都需要记录状态,但各自还需要不同的配置。
本节通过结构体嵌套提取公共部分。这种写法可以实现类似继承的数据复用,C 代码仍然需要显式访问内嵌成员、调用公共函数。
找出重复的字段
分别定义三种 LED 时,可能会重复填写公共成员:
1 | typedef struct { |
这三个类型的前四个字段相同。如果要增加公共字段 name,就需要修改三个定义。初始化标志检查等公共操作,也容易在多个函数中重复。
提取基础结构体
将共同字段放进 LedBase_t:
1 | typedef struct { |
具体类型再将它作为成员,并添加各自需要的数据:
1 | typedef struct { |
base 保存公共部分,pwm_channel、period_ms 和 increasing 保存具体实现所需的数据。
| 写法 | 含义 |
|---|---|
LedBase_t base; |
在对象中保存公共数据 |
gpio_led.base.pin |
访问公共成员 |
led_base_on(&obj->base) |
将公共部分交给基础函数处理 |
| 具体结构体新增成员 | 保存该类型独有的数据 |
公共函数接收 LedBase_t *
公共函数只需要接收基础部分的指针:
1 | int led_base_init(LedBase_t *me, uint8_t pin) |
具体类型初始化时,先调用公共初始化函数,再处理自己的配置:
1 | int gpio_led_init(GpioLed_t *me, uint8_t pin) |
GPIO LED 直接复用基础初始化。PWM LED 在基础初始化成功后,再保存通道并调用 pwm_init()。
这里仍沿用前面的简化实现,基础函数中还包含 GPIO 操作。结构体嵌套已经复用了数据和部分逻辑,但要让不同硬件共用同一操作接口,还需要后面的函数指针和操作表。
基础成员的布局与取址
本系列将 base 放在具体结构体的第一个成员:
1 | typedef struct { |
因此对象起始位置就是基础成员。取得基础指针时,直接使用 LedBase_t *base = &pwm_led.base; 即可。
需要区分两个概念:把 base 放在首位是一种布局约定,而 &pwm_led.base 是对成员取地址。即使成员不在首位,也可以这样取址。后面的 container_of 会根据成员偏移计算完整对象的地址。
复用数据后仍需处理行为差异
不同 LED 的点亮方式并不相同:GPIO LED 设置电平,PWM LED 设置占空比,RGB LED 可能控制三个通道,呼吸灯还需要定时改变亮度。
如果基础函数固定调用 gpio_write(),它就不能直接完成其他硬件的点亮操作。本节的结构体嵌套解决了公共数据和已有公共逻辑的复用;各类型的行为差异会在后续章节通过函数指针和 ops 处理。
与 C++ 继承的对照
C++ 可以用基类和派生类组织这些数据:
1 | // C++ |
C 则显式嵌入基础结构体:
1 | // C |
两种写法在数据组织上的对应如下:
| C++ | 本例中的 C 写法 |
|---|---|
基类 Base |
基础结构体 LedBase_t |
class Derived : public Base |
在具体类型中嵌入 LedBase_t base |
| 访问基类数据 | 通过 base.xxx 访问成员 |
| 调用基类操作 | 显式调用 led_base_xxx(&obj->base) |
| 派生类自身的数据 | 具体结构体新增的成员 |
结构体嵌套不会自动获得 C++ 的访问控制和类型转换规则。本系列借用“基类”“派生类型”等术语描述公共部分与具体实现,实际操作仍以 C 的成员访问和函数调用为准。
类似的组织方式也可用于存储设备(Flash、EEPROM、SD 卡)、通信接口(UART、SPI、I2C)或传感器(温度、湿度、IMU)。同类设备确实需要共用状态、配置或错误处理时,可以考虑提取公共部分。
下一篇先介绍单个函数指针,说明如何在保留对象参数的同时替换要执行的操作。










