07 继承:struct嵌套消灭复制粘贴
C 语言 OOP 系列导航
- C语言你真的会封装吗?
- 三个LED写了三份代码?
- 同事改一行,LED全乱了
- 手搓class:前缀、init/deinit和生命周期
- 你的全局变量,该死了
- 工业库也是同一套封装套路
- struct嵌套消灭复制粘贴 ⇐ 当前位置
- 写死的函数怎么换
- 把一组函数指针装进对象
- 一个指针管所有LED
- 虚函数不实现会怎样
- 换硬件不改应用
- 从自动注册到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
| 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
| 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 判断类型。