10 多态与转型:base 指针与 dispatch

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 结构

现在我们已经有了两个零件:公共字段 base,以及行为表 ops

多态要解决的问题是:上层不想知道这颗 LED 到底是 GPIO LED、PWM LED 还是 RGB LED。上层只想写:

1
led_on(led);

具体怎么点亮,由对象自己决定。

一句话先懂:C 语言的多态,就是上层只拿 base 指针,统一调用 dispatch 函数,再由 ops 分发到具体实现。

先看问题:if/else 会把具体类型泄漏到上层

没有多态时,上层往往会这样写:

1
2
3
4
5
6
7
if (type == LED_TYPE_GPIO) {
gpio_led_on(&gpio_led);
} else if (type == LED_TYPE_PWM) {
pwm_led_on(&pwm_led);
} else if (type == LED_TYPE_RGB) {
rgb_led_on(&rgb_led);
}

这段代码的问题不是 if/else 本身,而是上层知道了所有具体类型。

问题 后果
上层知道所有具体类型 抽象被打穿
新增类型要改分支 旧代码不断膨胀
调用入口不统一 管理器越来越复杂
具体硬件泄漏到业务层 换硬件成本高

目标是把这些分支藏到对象自己的 ops 里。

向上转型:从具体对象拿到 base 指针

具体类型把 LedBase_t 放在结构体里:

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

上层只拿基础部分:

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

这就是 C 语言里的向上转型。

概念 C 写法
具体对象 PwmLed_t pwm_led
基础部分 pwm_led.base
基础指针 LedBase_t *led = &pwm_led.base
上层只看抽象 只保存 LedBase_t *

dispatch:统一入口完成行为分发

不要让外部到处写 led->ops->on(led)。更好的做法是提供统一入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int led_on(LedBase_t *me)
{
if (me == NULL) {
return -1;
}

if (me->ops == NULL) {
return -2;
}

if (me->ops->on == NULL) {
return -3;
}

return me->ops->on(me);
}

其他接口同理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int led_off(LedBase_t *me)
{
if (me == NULL || me->ops == NULL || me->ops->off == NULL) {
return -1;
}

return me->ops->off(me);
}

int led_set_brightness(LedBase_t *me, uint8_t brightness)
{
if (me == NULL || me->ops == NULL || me->ops->set_brightness == NULL) {
return -1;
}

return me->ops->set_brightness(me, brightness);
}

调用链路展开后是:

1
2
3
led_on(led)
-> led->ops->on(led)
-> gpio_led_on / pwm_led_on / rgb_led_on

上层调用同一个 led_on(),具体实现由 ops 决定。

管理器:一个数组保存不同类型

有了基础指针,管理器不需要关心具体类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define LED_MANAGER_MAX_COUNT 8

typedef struct {
LedBase_t *items[LED_MANAGER_MAX_COUNT];
uint8_t count;
} LedManager_t;

int led_manager_add(LedManager_t *me, LedBase_t *led)
{
if (me == NULL || led == NULL) {
return -1;
}

if (me->count >= LED_MANAGER_MAX_COUNT) {
return -2;
}

me->items[me->count++] = led;
return 0;
}

统一操作只需要遍历基础指针:

1
2
3
4
5
6
7
8
9
10
void led_manager_turn_all_on(LedManager_t *me)
{
if (me == NULL) {
return;
}

for (uint8_t i = 0; i < me->count; i++) {
led_on(me->items[i]);
}
}

使用时可以加入不同具体类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GpioLed_t gpio_led;
PwmLed_t pwm_led;
RgbLed_t rgb_led;

gpio_led_init(&gpio_led, 5);
pwm_led_init(&pwm_led, 6, 1);
rgb_led_init(&rgb_led, 7, 8, 9);

LedManager_t manager = {0};

led_manager_add(&manager, &gpio_led.base);
led_manager_add(&manager, &pwm_led.base);
led_manager_add(&manager, &rgb_led.base);

led_manager_turn_all_on(&manager);

管理器全程只与 LedBase_t * 打交道。

向下转型:从 base 找回具体对象

具体实现函数收到的是 LedBase_t *,但有时需要访问派生类型自己的字段。比如 PWM LED 需要 pwm_channel

1
2
3
4
5
6
7
8
9
static int pwm_led_on(LedBase_t *base)
{
PwmLed_t *me = container_of(base, PwmLed_t, base);

me->base.is_on = true;
pwm_set_duty(me->pwm_channel, me->base.brightness);

return 0;
}

container_of 的本质是:已知结构体某个成员的地址,反推出整个结构体的起始地址。

1
2
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))

这就是 C 语言里的向下转型。

新人常见误区

误区 更准确的理解
多态就是函数指针 还需要基础指针、统一入口和对象绑定
上层可以直接调 ops 最好通过 dispatch 统一检查和默认处理
container_of 很神秘 它只是用成员地址减去成员偏移
所有分支都要消灭 具体实现内部仍然可以有必要分支

C/C++ 对照

C++ C
基类指针 Led * LedBase_t *
派生类对象 内嵌 LedBase_t base 的结构体
向上转型 &derived.base
虚函数调用 me->ops->on(me)
动态绑定 初始化时绑定不同 ops
向下转型 container_of

嵌入式项目意义

多态真正解决的是上层稳定:

没有多态 有多态
上层判断具体类型 上层只认基础接口
新增类型要改管理器 新增类型只注册新对象
硬件细节到处泄漏 硬件差异封在具体实现里
代码越写越多分支 代码围绕统一接口扩展

这对驱动框架、设备管理、协议栈都非常重要。

总结

C 语言的多态 = base 指针 + ops 操作表 + dispatch 函数;上层面向抽象,具体对象自己决定行为。