接口设计:ops 合同与默认实现
11 接口设计:ops 合同与默认实现
C 语言 OOP 系列导航
本系列面向会 C 和单片机基础、刚进入企业项目的同学,从
struct封装一路讲到ops、多态和类 Linux 驱动框架。
ops 表把 C 语言带到了虚函数层级,但它也带来一个新风险:函数指针可能是 NULL。
例如操作表定义如下:
1 | typedef struct { |
某个具体实现可能没有提供 deinit:
1 | static const LedOps_t pwm_led_ops = { |
如果 dispatch 直接调用:
1 | return me->ops->deinit(me); |
就会空指针解引用。
一句话先懂:ops 不是函数指针杂货铺,它是一份接口合同,必须明确哪些能力必选、哪些能力可选、不支持时怎么办。
先看问题:槽位缺失不能靠调用方猜
| 问题 | 后果 |
|---|---|
| ops 槽位没实现 | 空指针调用 |
| 上层不知道可选 / 必选 | 接口合同不清楚 |
| 每个地方自己检查 | 错误处理不一致 |
| 没有默认实现 | 简单设备也要写重复函数 |
接口设计的目标不是“把函数都塞进表里”,而是让调用方知道什么一定能用,什么可能不可用。
先定义接口合同
设计 ops 时,先约定每个槽位的性质:
| 函数 | 是否必须 | 说明 |
|---|---|---|
on |
必须 | LED 至少要能点亮 |
off |
必须 | LED 至少要能关闭 |
set_brightness |
可选 | 普通 GPIO LED 可能不支持真实调光 |
deinit |
可选或必须 | 看框架是否负责资源释放 |
这份约定不能只写在脑子里,要体现在代码检查里。
必选函数在绑定时检查
dispatch 做 NULL 检查可以避免崩溃,但更好的做法是对象绑定 ops 时就拒绝不合规实现:
1 | static bool led_ops_is_valid(const LedOps_t *ops) |
这样 on/off 这种必选能力从一开始就被保证了。
可选函数在 dispatch 中处理
deinit 如果是可选,就要在统一入口里处理:
1 | int led_deinit(LedBase_t *me) |
这样调用方得到的是明确错误码,而不是系统直接跑飞。
默认实现承接常见可选能力
有些可选能力可以给默认实现。例如普通 GPIO LED 不支持真实亮度,但可以把 brightness == 0 理解为关灯,其他值理解为开灯:
1 | static int led_default_set_brightness(LedBase_t *me, uint8_t brightness) |
dispatch 中优先调用具体实现,没有实现就走默认实现:
1 | int led_set_brightness(LedBase_t *me, uint8_t brightness) |
这能减少简单设备的重复模板代码。
Linux 风格接口为什么常允许 NULL
大型 C 框架里,很多 ops 槽位都是可选的:
1 | typedef struct { |
不是所有设备都支持 ioctl,所以调用前必须检查:
1 | int device_ioctl(Device_t *dev, int cmd, void *arg) |
允许 NULL 没问题,前提是接口合同明确,并且统一入口负责处理。
新人常见误区
| 误区 | 更稳的做法 |
|---|---|
| 所有槽位都填满才专业 | 能默认的默认,不能支持的明确返回错误 |
| 每个调用点自己判空 | 集中在 dispatch 中处理 |
NULL 表示没关系 |
NULL 必须有明确语义 |
| ops 表越大越好 | 只放稳定、真正属于对象的能力 |
纯虚、虚函数与默认实现
| 概念 | C++ | C 语言 |
|---|---|---|
| 纯虚函数 | virtual void on() = 0 |
ops 槽位必须非 NULL |
| 虚函数 | virtual void on() |
ops 槽位可被具体实现覆盖 |
| 默认实现 | 基类提供实现 | dispatch 里走默认函数 |
| 接口合同 | 抽象类声明 | ops_is_valid() 检查 |
C 语言不会在编译期校验“纯虚函数是否被实现”,所以这份职责要由初始化代码承担。
嵌入式项目意义
驱动接口必须回答这些问题:
| 设计问题 | 应该明确 |
|---|---|
| 哪些操作必须支持 | 初始化时检查 |
| 哪些操作可以不支持 | dispatch 返回错误或默认实现 |
| 不支持时返回什么 | 统一错误码 |
| 默认行为是什么 | 写成明确函数 |
| 谁负责释放资源 | deinit 合同 |
否则 ops 会从解耦工具变成空指针雷区。
总结
ops是接口合同,不是函数指针杂货铺;必选函数初始化时检查,可选函数调用前判断,能默认实现就集中默认实现。








