11 接口设计:虚函数不实现会怎样

C 语言 OOP 系列导航

  1. C语言你真的会封装吗?
  2. 三个LED写了三份代码?
  3. 同事改一行,LED全乱了
  4. 手搓class:前缀、init/deinit和生命周期
  5. 你的全局变量,该死了
  6. 工业库也是同一套封装套路
  7. struct嵌套消灭复制粘贴
  8. 写死的函数怎么换
  9. 把一组函数指针装进对象
  10. 一个指针管所有LED
  11. 虚函数不实现会怎样 ⇐ 当前位置
  12. 换硬件不改应用
  13. 从自动注册到Linux内核OOP全貌

有了 ops 表,就有了 C 语言版虚函数。但如果某个函数指针是 NULL,会怎样?答案很直接:不检查就调用,程序直接崩。

ops 槽位为空

假设有人偷懒把 deinit 写成 NULL

1
2
3
4
5
6
static const LedOps_t pwm_led_ops = {
.on = pwm_led_on,
.off = pwm_led_off,
.set_brightness = pwm_led_set_brightness,
.deinit = NULL,
};

如果 dispatch 直接 return me->ops->deinit(me);,就是空函数指针调用 — 比普通返回错误更危险。

接口合同:先定义清楚哪些必须实现

函数 是否必须 说明
on 必须 LED 至少要能点亮
off 必须 LED 至少要能关闭
set_brightness 可选 普通 GPIO LED 可能不支持真实调光
deinit 可选或必须 看框架是否负责资源释放

这个合同要体现在代码里。初始化时检查必选函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static bool led_ops_is_valid(const LedOps_t *ops)
{
if (ops == NULL) {
return false;
}

if (ops->on == NULL || ops->off == NULL) {
return false;
}

return true;
}

int led_base_bind_ops(LedBase_t *me, const LedOps_t *ops)
{
if (me == NULL || !led_ops_is_valid(ops)) {
return -1;
}

me->ops = ops;
return 0;
}

默认实现:简单设备不用重复写

普通 GPIO LED 不支持真实亮度调节,但可以给一个默认实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int led_default_set_brightness(LedBase_t *me, uint8_t brightness)
{
if (me == NULL) {
return -1;
}

me->brightness = brightness;

if (brightness == 0) {
return led_off(me);
}

return led_on(me);
}

dispatch 里先查具体实现,没有就走默认:

1
2
3
4
5
6
7
8
9
10
11
12
int led_set_brightness(LedBase_t *me, uint8_t brightness)
{
if (me == NULL || me->ops == NULL) {
return -1;
}

if (me->ops->set_brightness != NULL) {
return me->ops->set_brightness(me, brightness);
}

return led_default_set_brightness(me, brightness);
}

简单设备不用重复写一堆模板函数。

C 语言 vs C++ 的接口机制

概念 C++ C 语言
纯虚函数 virtual void on() = 0 ops 槽位必须非 NULL
虚函数 virtual void on() ops 槽位可被覆盖
默认实现 基类提供实现 dispatch 里走默认函数
接口合同 抽象类声明 ops_is_valid() 检查

C 语言没有编译器帮你检查纯虚函数是否实现,所以你要自己写检查。

Linux 风格接口为什么常有 NULL

很多大型 C 框架把 ops 设计成”部分可选”。比如不是每个设备都支持 ioctl,所以调用前必须检查:

1
2
3
4
5
6
7
8
int device_ioctl(Device_t *dev, int cmd, void *arg)
{
if (dev == NULL || dev->ops == NULL || dev->ops->ioctl == NULL) {
return -1;
}

return dev->ops->ioctl(dev, cmd, arg);
}

关键是:文档和代码都要说明哪些函数可选。

驱动接口不是堆函数指针

在设计 ops 时,必须想清楚:

设计问题 应该明确
哪些操作必须支持 初始化时检查
哪些操作可以不支持 dispatch 返回错误或默认实现
不支持时返回什么 统一错误码
默认行为是什么 写成明确函数
谁负责释放资源 deinit 合同

否则 ops 会从”解耦工具”变成”空指针雷区”。

ops 是接口合同,不是函数指针杂货铺。必选函数初始化时检查,可选函数调用前判断,能默认实现就集中默认实现。