03 信息隐藏:公共接口与内部实现
03 信息隐藏:公共接口与内部实现
C 语言面向对象系列
上一节用结构体保存 LED 的数据,再通过对象指针调用函数。本节继续处理一个问题:如果外部代码直接修改结构体成员,软件记录的状态可能与硬件不一致。
直接修改成员会绕过硬件操作
继续使用这个 LED 类型:
1 | typedef struct { |
点亮 LED 时,调用接口:
1 | led_on(&red_led); |
如果改为直接给成员赋值:
1 | red_led.is_on = true; |
这条语句只修改 is_on,并没有执行对应的 GPIO 操作:
1 | gpio_write(red_led.pin, true); |
假设 LED 原本处于关闭状态,此时 is_on 已经是 true,LED 却仍然没有点亮。后续代码如果根据这个字段判断状态,就可能得到错误结果,日志记录也会与硬件表现不一致。
因此,修改状态的接口需要同时处理对应的硬件操作。信息隐藏的作用,就是减少外部代码绕过这些接口的机会。
头文件声明接口,源文件组织实现
一个 C 模块通常由头文件和源文件组成:
| 文件 | 主要内容 |
|---|---|
.h |
供外部使用的类型和函数声明 |
.c |
函数实现、内部辅助函数和模块状态 |
LED 模块可以先提供以下头文件:
1 |
|
本例仍在头文件中公开结构体定义,因此调用方在语法上可以访问成员。这里先约定通过 led_xxx() 接口读写状态;需要更严格的信息隐藏时,可以使用不透明结构体,将完整定义放在源文件中。
用 static 限定内部辅助函数
如果多个接口各自操作 GPIO,代码会出现重复:
1 | int led_on(Led_t *me) |
硬件改成低电平点亮时,这些位置都需要修改。可以先把硬件更新集中到一个辅助函数中:
1 | static void led_update_hardware(Led_t *me) |
公开接口负责修改状态,再调用这个函数更新硬件:
1 | int led_on(Led_t *me) |
这里的 static 使 led_update_hardware() 具有内部链接,其他源文件不能通过这个函数名直接调用它。实际组织模块时,可以将这理解为当前源文件内部使用的函数。
它与 C++ 的 private 方法都有隐藏实现的用途,但约束范围不同:static 在这里控制函数的链接范围,private 控制类成员的访问权限。给辅助函数加 static,也不会自动隐藏头文件里公开的结构体成员。
通过查询接口读取状态
读取状态也可以集中到一个函数中。这类只负责读取的接口通常称为 getter:
1 | int led_get_state(const Led_t *me, bool *is_on, uint8_t *brightness) |
这个接口有三个需要注意的地方:
| 写法 | 作用 |
|---|---|
const Led_t *me |
函数不通过 me 修改对象成员 |
检查输出指针是否为 NULL |
调用方可以只读取需要的字段 |
| 返回错误码 | 对象指针为 NULL 时报告失败 |
调用时,先检查返回值,再使用读取结果:
1 | bool is_on; |
查询和修改分别通过不同接口完成,也便于定位状态是在何处发生变化的。
统一模块的访问方式
这份 LED 模块可以采用以下约定:
| 操作 | 使用方式 |
|---|---|
| 点亮 LED | 调用 led_on(&led),由接口更新状态和硬件 |
| 熄灭 LED | 调用 led_off(&led),由接口更新状态和硬件 |
| 查询状态 | 调用 led_get_state() |
| 更新 GPIO | 集中在内部辅助函数中 |
| 声明内部辅助函数 | 放在源文件中,并用 static 限定链接范围 |
从职责上看,头文件中的公开函数类似 C++ 类的公共接口,源文件中的辅助函数负责内部实现。外部调用统一接口后,硬件改动主要在模块内部处理,也更容易检查软件状态与硬件操作是否一致。










