09 操作表:使用 ops 组织对象接口
C 语言面向对象系列
总纲:C 语言面向对象设计概览
封装:结构体与多实例接口
信息隐藏:公共接口与内部实现
模块组织:命名约定与对象生命周期
数据组织:实例状态、模块状态与常量
HAL 实例:STM32 外设接口中的封装设计
结构体嵌套:公共数据与行为的复用
函数指针:可替换行为与回调
操作表:使用 ops 组织对象接口 (当前篇)
多态:统一接口、行为分发与类型转换
接口约定:必选操作、可选操作与默认实现
框架整合:应用、驱动与平台的分层
工程扩展:自动注册与 Linux 内核中的对象组织
函数指针可以替换一个操作,而设备通常需要一组相关操作。例如 LED 有开启、关闭、亮度设置和释放资源;文件有打开、读取、写入和关闭;网卡也有启动、停止和发送等操作。
本节将这些相关函数指针放进一个结构体,称为操作表,通常命名为 ops。
将相关操作放在一起 如果每次调用都单独传入具体函数,代码可能呈现为下面的形式:
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);
调用方需要分别选择每个函数。对于同一种设备,这些操作通常应来自同一个实现:
操作
GPIO LED
PWM LED
on
输出高电平
设置 PWM 占空比
off
输出低电平
将占空比设为 0
set_brightness
保存数值,并按零与非零设置开关
设置或保存占空比
deinit
释放 GPIO 配置
按资源约定停止或释放 PWM
可以把一组实现集中存放,由对象记录使用哪一组。
定义操作表类型 操作函数需要接收 LedBase_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;
每个成员都是函数指针。on、off、deinit 接收对象指针,set_brightness 还接收亮度参数。
在对象中保存操作表指针 基础结构体增加 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 实现操作表 先实现各个操作,再将函数填入 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, };
这些函数处理 GPIO LED 的开关、亮度记录和资源释放。其中 set_brightness 仍然只按零与非零设置电平,并没有实现 PWM 调光。
初始化对象时,将它绑定到这份操作表:
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 ; }
me->base.ops = &gpio_led_ops 建立了对象与实现之间的关系。其他 GPIO LED 实例也可以使用同一份表,各自保留独立状态。
为 PWM LED 提供另一份实现 PWM LED 使用相同的 LedOps_t 类型,但填入不同函数:
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,表示这份表没有提供该操作。调用方必须处理缺失操作,第 11 篇会说明必选操作、可选操作和默认实现。
第二,这段简化代码仍通过 base.pin 调用 pwm_set_duty(),尚未读取具体结构体中的 pwm_channel。如果底层接口接收 PWM 通道号,就需要从基础指针取得具体对象,再读取通道成员;第 10 篇会展示这一过程。
通过操作表调用函数 对象完成初始化并绑定有效操作表后,可以沿着 ops 找到对应函数:
1 2 3 led->ops->on(led); led->ops->set_brightness(led, 80 ); led->ops->off(led);
以 led->ops->on(led) 为例:先取得对象的操作表,再读取 on 函数指针,最后将当前对象传给它。
这种直接调用要求对象、操作表和相应函数指针都有效。下一篇会提供统一入口,将检查和调用放在同一个函数中。
对象中的内容
用途
普通成员
保存配置和运行状态
ops 指针
指向该对象使用的一组操作
这种操作表与 C++ 实现虚函数调用时常用的虚函数表(vtable)用途相近。这里的表和分发过程由 C 代码显式定义,具体的类型检查和接口约定也需要自行处理。
将操作表用于其他接口 同样的组织方式可以用于存储驱动的 read/write/erase、通信接口的 send/recv/ioctl、文件系统的 open/read/write/close,以及网络设备的 open/stop/transmit。
当新硬件能够遵循现有接口约定时,可以增加对应的操作表及实现,让上层继续使用相同接口。