07 继承:struct嵌套消灭复制粘贴

C 语言 OOP 系列导航

  1. C语言你真的会封装吗?
  2. 三个LED写了三份代码?
  3. 同事改一行,LED全乱了
  4. 手搓class:前缀、init/deinit和生命周期
  5. 你的全局变量,该死了
  6. 工业库也是同一套封装套路
  7. struct嵌套消灭复制粘贴 ⇐ 当前位置
  8. 写死的函数怎么换
  9. 把一组函数指针装进对象
  10. 一个指针管所有LED
  11. 虚函数不实现会怎样
  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;

前四个字段完全重复。如果公共字段要加一个 name,三个结构体都要改。如果公共函数要检查 initialized,三套函数都要写。

抽出 LedBase_t

继承的核心不是语法,而是复用公共部分。先抽出基础结构体:

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

然后具体 LED 内嵌它:

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_on(&obj->base) 复用父类行为
具体结构体新增字段 派生类自己的数据

公共函数只操作 base

公共逻辑写在 base 层:

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
17
18
19
20
21
22
23
24
25
int gpio_led_init(GpioLed_t *me, uint8_t pin)
{
if (me == NULL) {
return -1;
}

return led_base_init(&me->base, pin);
}

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;

这样对象开头就是 base。后续做向上转型时可以安全拿到:LedBase_t *base = &pwm_led.base;。这个习惯后面讲 container_of 时会用到。

继承解决了一部分,行为差异还没解决

继承后,公共字段复用了。但不同 LED 的点亮方式仍然不一样:GPIO LED 写电平,PWM LED 设占空比,RGB LED 同时控制三个通道,呼吸灯启动定时器渐变。如果 led_base_on() 写死为 gpio_write(),PWM LED 就不适用了。

所以继承只解决公共数据和公共基础逻辑,不解决行为差异。行为差异要靠后面的函数指针和 ops 表。

C/C++ 对照

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

class PwmLed : public LedBase {
private:
int pwm_channel;
};
1
2
3
4
5
6
7
8
9
10
11
// C
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/PWM/RGB)、存储(Flash/EEPROM/SD卡)、通信(UART/SPI/I2C)、传感器(温度/湿度/IMU)。它们常常有公共状态、公共配置、公共错误码。用 base 抽出来后,上层更容易统一管理。


下一篇我们解决行为差异的问题:怎么让 led_on() 在 GPIO LED 上写电平,在 PWM LED 上设占空比,而不用写一堆 if/else 判断类型。