09 ops/vtable:把一组函数指针装进对象
C 语言 OOP 系列导航
C语言你真的会封装吗?
三个LED写了三份代码?
同事改一行,LED全乱了
手搓class:前缀、init/deinit和生命周期
你的全局变量,该死了
工业库也是同一套封装套路
struct嵌套消灭复制粘贴
写死的函数怎么换
把一组函数指针装进对象 ⇐ 当前位置
一个指针管所有LED
虚函数不实现会怎样
换硬件不改应用
从自动注册到Linux内核OOP全貌
函数指针能替换一个动作。但一个对象通常不止一个动作——LED 有 on、off、set_brightness、deinit;文件有 open、read、write、release;网卡有 open、stop、start_xmit。
痛点:一个个传函数指针太散 如果只用单个函数指针:
1 2 3 led_run_action(led, gpio_led_on); led_run_action(led, gpio_led_off); led_run_set(led, gpio_led_set_brightness);
动作之间没有被组织起来。但对一个具体的 LED 来说,”一整套操作”应该是配套的:
操作
GPIO LED
PWM LED
on
写 GPIO 高电平
设置 PWM 占空比
off
写 GPIO 低电平
占空比设 0
set_brightness
保存亮度
改 PWM duty
deinit
释放 GPIO
停止 PWM
这组函数应该打包在一起。
定义 LedOps_t 先前置声明基础类型:
1 typedef struct LedBase LedBase_t ;
再定义操作表:
1 2 3 4 5 6 typedef struct { int (*on)(LedBase_t *me); int (*off)(LedBase_t *me); int (*set_brightness)(LedBase_t *me, uint8_t brightness); int (*deinit)(LedBase_t *me); } LedOps_t;
这就是 ops — 把一组函数指针打包成一张表。
把 ops 放进对象 基础对象保存一个 ops 指针:
1 2 3 4 5 6 7 struct LedBase { const LedOps_t *ops; uint8_t pin; uint8_t brightness; bool is_on; bool initialized; };
为什么是 const LedOps_t *?因为操作表通常是静态只读的,某种类型的操作表一般固定,不应该在运行时被随便改。
GPIO LED 提供自己的 ops 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 33 34 35 36 37 38 39 40 typedef struct { LedBase_t base; } GpioLed_t; static int gpio_led_on (LedBase_t *me) { me->is_on = true ; gpio_write(me->pin, true ); return 0 ; } static int gpio_led_off (LedBase_t *me) { me->is_on = false ; gpio_write(me->pin, false ); return 0 ; } static int gpio_led_set_brightness (LedBase_t *me, uint8_t brightness) { me->brightness = brightness; me->is_on = (brightness > 0 ); gpio_write(me->pin, me->is_on); return 0 ; } static int gpio_led_deinit (LedBase_t *me) { gpio_led_off(me); gpio_deinit(me->pin); me->initialized = false ; return 0 ; } static const LedOps_t gpio_led_ops = { .on = gpio_led_on, .off = gpio_led_off, .set_brightness = gpio_led_set_brightness, .deinit = gpio_led_deinit, };
初始化时绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int gpio_led_init (GpioLed_t *me, uint8_t pin) { if (me == NULL ) { return -1 ; } me->base.ops = &gpio_led_ops; me->base.pin = pin; me->base.brightness = 0 ; me->base.is_on = false ; me->base.initialized = true ; gpio_init(pin); return 0 ; }
PWM LED 也提供自己的 ops 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 33 34 35 36 typedef struct { LedBase_t base; uint8_t pwm_channel; } PwmLed_t; static int pwm_led_on (LedBase_t *me) { me->is_on = true ; pwm_set_duty(me->pin, me->brightness); return 0 ; } static int pwm_led_off (LedBase_t *me) { me->is_on = false ; pwm_set_duty(me->pin, 0 ); return 0 ; } static int pwm_led_set_brightness (LedBase_t *me, uint8_t brightness) { me->brightness = brightness; if (me->is_on) { pwm_set_duty(me->pin, brightness); } return 0 ; } static const LedOps_t pwm_led_ops = { .on = pwm_led_on, .off = pwm_led_off, .set_brightness = pwm_led_set_brightness, .deinit = NULL , };
这里故意把 deinit 写成 NULL — 接口缺失时必须处理,下一篇讲接口合同时会展开。
调用方式 有了 ops 后,调用形态是:
1 2 3 led->ops->on(led); led->ops->set_brightness(led, 80 ); led->ops->off(led);
从结构上看,对象现在有两部分:
内容
作用
普通字段
保存状态
ops 指针
指向行为表
对象不光”有数据”,还”知道自己该怎么被操作”。(C++ 里编译器生成 vtable 做这件事,C 里我们手动写出来,但思路完全一样。)
嵌入式里的 ops ops 在驱动设计中到处可见 — LED 驱动(on/off/set_brightness)、存储驱动(read/write/erase)、通信接口(send/recv/ioctl)、文件系统(open/read/write/close)、网络设备(open/stop/transmit)。新增一种硬件时,只需要提供一套新的操作表,上层接口不用动。
单个函数指针替换一个动作,ops 操作表替换一整套行为。这就是 C 语言里的 vtable。