09 ops/vtable:行为表与对象绑定
C 语言 OOP 系列导航
本系列面向会 C 和单片机基础、刚进入企业项目的同学,从 struct 封装一路讲到 ops、多态和类 Linux 驱动框架。
C 语言面向对象设计路线
封装:struct 数据与 me 指针
信息隐藏:static 与头文件边界
类的组织:前缀、init/deinit 与生命周期
数据归位:struct 成员与 static 变量
HAL 映射:工业库的封装结构
继承:struct 嵌套复用公共字段
函数指针:把行为作为参数
ops/vtable:行为表与对象绑定 ⇐ 当前位置
多态与转型:base 指针与 dispatch
接口设计:ops 合同与默认实现
完整框架:抽象层与硬件解耦
终章:自动注册与 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);
一句话先懂:ops 就是一张行为表,把一个对象的一整套动作打包在一起,再绑定到对象身上。
先看问题:动作应该成套出现 对一个具体 LED 来说,on/off/set_brightness/deinit 是配套的:
操作
GPIO LED
PWM LED
on
写 GPIO 高电平
设置 PWM 占空比
off
写 GPIO 低电平
占空比设 0
set_brightness
保存亮度或模拟开关
改 PWM duty
deinit
释放 GPIO
停止 PWM
这些函数不应该散落在调用点,而应该作为一个整体描述“这种 LED 怎么工作”。
定义 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;
这张表不是数据状态,而是一组行为入口。C++ 里编译器会帮你生成 vtable;C 语言里我们手动写出这张表。
把 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 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 ; }
再把函数填入一张静态表:
1 2 3 4 5 6 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 PWM LED 的数据和行为不同,所以它有自己的操作表:
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 指针
指向行为表
也就是说,对象不仅有数据,还知道“自己应该用哪套函数操作”。
新人常见误区
误区
更准确的理解
ops 只是函数指针数组
它表达的是一组接口能力
每个对象都要复制一张 ops
通常同类型对象共用一张 static const 表
ops 可以运行时随便改
除非明确需要热切换,否则初始化后应保持稳定
有了 ops 就可以直接调用
真实项目里应通过 dispatch 统一检查
C/C++ 对照
C++
C
虚函数
函数指针
虚函数表 vtable
ops 结构体
对象里的 vptr
const LedOps_t *ops
obj->on()
obj->ops->on(obj)
纯虚接口
只定义函数指针槽位
嵌入式项目意义 ops 在驱动设计里非常常见:
场景
ops 示例
LED 驱动
on/off/set_brightness
存储驱动
read/write/erase
通信接口
send/recv/ioctl
文件系统
open/read/write/close
网络设备
open/stop/transmit
新增一种硬件时,只需要提供一张新的操作表,上层接口可以保持稳定。
总结
单个函数指针替换一个动作,ops 操作表替换一整套行为;这就是 C 语言里的 vtable。