11 接口约定:必选操作、可选操作与默认实现
11 接口约定:必选操作、可选操作与默认实现
C 语言面向对象系列
操作表中的函数指针可能为 NULL。设计接口时,需要先约定哪些操作必须提供、哪些允许缺省,以及缺省时如何处理。
本节在前面的 ops 基础上加入这些约定,让调用方和实现方遵循同一规则。
明确空函数指针的含义
例如,这份 PWM 操作表没有提供 deinit:
1 | static const LedOps_t pwm_led_ops = { |
如果统一入口直接执行 return me->ops->deinit(me);,就会调用空函数指针。这属于未定义行为,不能依靠它返回错误,程序也可能发生异常。
NULL 是否允许出现,需要由接口约定决定。调用入口必须据此检查、返回错误,或执行默认操作。
绑定操作表时检查必选操作
可以先为 LED 接口作出如下约定:
| 操作 | 要求 | 说明 |
|---|---|---|
on |
必须提供 | 支持点亮 |
off |
必须提供 | 支持关闭 |
set_brightness |
可以缺省 | 普通 GPIO LED 可能没有实际调光能力 |
deinit |
由资源管理方式决定 | 明确是否需要由该接口释放资源 |
必选操作可以在绑定操作表时统一检查:
1 | static bool led_ops_is_valid(const LedOps_t *ops) |
led_ops_is_valid() 检查操作表以及 on、off。led_base_bind_ops() 只在检查通过后记录指针。调用方需要处理绑定失败,避免继续使用未完成绑定的对象。
如果框架规定 deinit 必须实现,就还需要将它纳入检查。检查内容应与文档中的要求一致。
为可选操作提供默认实现
普通 GPIO LED 不能通过占空比调节亮度,但可以约定一种简化行为:亮度为 0 时关闭,非零时开启。
1 | static int led_default_set_brightness(LedBase_t *me, uint8_t brightness) |
统一入口优先使用具体设备提供的函数;没有提供时,再调用默认函数:
1 | int led_set_brightness(LedBase_t *me, uint8_t brightness) |
这样,采用同一种默认行为的设备无需重复实现函数。这个默认行为只提供开关控制,调用方仍需要知道它与实际调光的区别。
将不支持的操作报告给调用方
有些操作没有合适的默认实现,就应返回错误。例如,设备不一定支持 ioctl:
1 | int device_ioctl(Device_t *dev, int cmd, void *arg) |
这里依次检查设备、操作表和函数指针,缺少任何一项就返回错误。大型 C 框架中的可选操作也需要类似的调用约定;具体哪些函数可选,应查对应接口文档。
与 C++ 接口机制的对照
C++ 可以用抽象类和虚函数描述类似要求:
| 设计用途 | C++ 写法 | 本系列的 C 写法 |
|---|---|---|
| 要求具体类型提供操作 | 纯虚函数,例如 virtual void on() = 0 |
将对应 ops 成员列为必选并检查 |
| 允许不同类型分别实现 | 虚函数,例如 virtual void on() |
为各类型绑定不同函数指针 |
| 共用默认行为 | 基类提供实现 | 在统一入口中调用默认函数 |
| 检查接口是否满足要求 | 编译器根据类定义检查 | 通过 ops_is_valid() 等函数检查 |
两者的检查方式不同。C 中声明一个函数指针成员,并不意味着编译器会要求所有操作表都填写这个成员,因此需要代码和文档共同表达要求。
将接口约定落实到代码
设计操作表时,可以逐项确认:
| 内容 | 在代码中的安排 |
|---|---|
| 必须支持的操作 | 初始化或绑定时检查 |
| 允许缺省的操作 | 调用前判断 |
| 不支持操作时的结果 | 返回统一约定的错误码 |
| 缺省时的行为 | 集中实现默认函数 |
| 资源的释放责任 | 明确 deinit 的要求和调用时机 |
操作表提供了调用入口,接口约定则说明这些入口何时可用、失败时返回什么,以及调用方需要承担哪些责任。








