07 继承:struct 嵌套复用公共字段

C 语言 OOP 系列导航

本系列面向会 C 和单片机基础、刚进入企业项目的同学,从 struct 封装一路讲到 ops、多态和类 Linux 驱动框架。

  1. C 语言面向对象设计路线
  2. 封装:struct 数据与 me 指针
  3. 信息隐藏:static 与头文件边界
  4. 类的组织:前缀、init/deinit 与生命周期
  5. 数据归位:struct 成员与 static 变量
  6. HAL 映射:工业库的封装结构
  7. 继承:struct 嵌套复用公共字段 ⇐ 当前位置
  8. 函数指针:把行为作为参数
  9. ops/vtable:行为表与对象绑定
  10. 多态与转型:base 指针与 dispatch
  11. 接口设计:ops 合同与默认实现
  12. 完整框架:抽象层与硬件解耦
  13. 终章:自动注册与 Linux 内核 OOP 结构

封装能组织好单个对象。但项目里经常会出现“一类设备的多种实现”:GPIO LED、PWM LED、RGB LED、呼吸灯。

它们的点亮方式不同,但都有一些共同状态:引脚、亮度、是否点亮、是否初始化。

如果每种 LED 都独立定义结构体,公共字段会被复制很多次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
bool initialized;
} GpioLed_t;

typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
bool initialized;
uint8_t pwm_channel;
} PwmLed_t;

typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
bool initialized;
uint16_t period_ms;
} BreathingLed_t;

一句话先懂:C 语言里的继承,就是把公共字段抽成一个 base struct,再把它嵌进具体结构体里。

先看问题:重复字段会让修改变成体力活

前四个字段在三个结构体里完全一样。以后新增一个公共字段,比如 nameerror_code,就要改三处。公共检查逻辑也会重复。

问题 后果
公共字段复制 改字段要改多处
公共逻辑复制 bug 修复容易漏
类型之间无关系 上层无法统一管理
新增 LED 类型 继续复制一份

这时需要的不是再封装一个函数,而是把公共部分抽出来。

抽出 LedBase_t

先定义基础结构体:

1
2
3
4
5
6
typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
bool initialized;
} LedBase_t;

具体类型把它作为成员嵌进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
LedBase_t base;
} GpioLed_t;

typedef struct {
LedBase_t base;
uint8_t pwm_channel;
} PwmLed_t;

typedef struct {
LedBase_t base;
uint16_t period_ms;
bool increasing;
} BreathingLed_t;

关系可以这样理解:

C 写法 设计含义
LedBase_t base; 继承公共字段
gpio_led.base.pin 访问父类字段
led_base_xxx(&obj->base) 复用父类行为
具体结构体新增字段 派生类自己的数据

公共函数只操作 base

公共逻辑应该收敛到操作 LedBase_t * 的函数里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int led_base_init(LedBase_t *me, uint8_t pin)
{
if (me == NULL) {
return -1;
}

me->pin = pin;
me->brightness = 0;
me->is_on = false;
me->initialized = true;

gpio_init(pin);
gpio_write(pin, false);

return 0;
}

int led_base_off(LedBase_t *me)
{
if (me == NULL) {
return -1;
}

if (!me->initialized) {
return -2;
}

me->is_on = false;
gpio_write(me->pin, false);

return 0;
}

具体类型的初始化先复用基础逻辑,再补自己的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int pwm_led_init(PwmLed_t *me, uint8_t pin, uint8_t channel)
{
if (me == NULL) {
return -1;
}

int ret = led_base_init(&me->base, pin);
if (ret != 0) {
return ret;
}

me->pwm_channel = channel;
pwm_init(channel);

return 0;
}

公共部分只写一遍,具体差异留给具体类型。

base 通常放在第一个成员

工程上常把 base 放在结构体第一个成员:

1
2
3
4
typedef struct {
LedBase_t base;
uint8_t pwm_channel;
} PwmLed_t;

这样做有两个好处:

好处 说明
向上转型简单 可以把 &pwm_led.base 当成基础对象指针
后续反推方便 container_of 能从 base 找回完整对象

示例:

1
2
PwmLed_t pwm_led;
LedBase_t *base = &pwm_led.base;

这一步会在后面的多态与转型里继续展开。

继承解决公共部分,不解决行为差异

继承能复用公共字段和公共基础逻辑,但不能自动解决“不同类型怎么点亮”的问题。

类型 点亮方式
GPIO LED 写 GPIO 电平
PWM LED 设置 PWM 占空比
RGB LED 同时控制三个通道
呼吸灯 启动定时器渐变

如果 led_base_on() 写死为 gpio_write(),PWM LED 就不合适。行为差异要靠函数指针和 ops 操作表解决。

新人常见误区

误区 更准确的理解
继承就是为了少写字段 更重要的是建立“这些对象属于同一类”的关系
base 可以随便放位置 建议放第一个成员,后续转型更清楚
有了继承就能多态 继承只解决公共数据,多态还需要行为分发
所有相似结构都要抽 base 只有公共字段和公共逻辑稳定时才抽

C/C++ 对照

C++ 风格:

1
2
3
4
5
6
7
8
9
10
11
class LedBase {
protected:
int pin;
int brightness;
bool is_on;
};

class PwmLed : public LedBase {
private:
int pwm_channel;
};

C 风格:

1
2
3
4
5
6
7
8
9
10
typedef struct {
int pin;
int brightness;
bool is_on;
} LedBase_t;

typedef struct {
LedBase_t base;
int pwm_channel;
} PwmLed_t;
C++ C
class Base LedBase_t
class Derived : public Base 结构体内嵌 LedBase_t base
父类字段 base.xxx
父类方法 led_base_xxx(&obj->base)
派生类字段 具体结构体自己的成员

嵌入式项目意义

嵌入式里经常有“抽象类型 + 多个具体实现”:

抽象类型 具体实现
LED GPIO LED、PWM LED、RGB LED
存储 Flash、EEPROM、SD 卡
通信 UART、SPI、I2C
传感器 温度、湿度、IMU

base 抽出公共部分后,上层更容易统一管理,也为后面的多态打基础。

总结

C 语言的继承不是 extends,而是把公共字段和公共行为抽进 base struct,再让具体对象把 base 嵌进去。