信息隐藏:static 与头文件边界
03 信息隐藏:static 与头文件边界
C 语言 OOP 系列导航
本系列面向会 C 和单片机基础、刚进入企业项目的同学,从
struct封装一路讲到ops、多态和类 Linux 驱动框架。
struct + me 解决了“一份函数操作多个对象”的问题,但还没解决“别人能不能绕过接口乱改状态”的问题。
例如 LED 对象里有一个 is_on:
1 | typedef struct { |
正常点亮应该调用接口:
1 | led_on(&red_led); |
如果外部直接改字段:
1 | red_led.is_on = true; |
软件状态变成“灯亮了”,但 GPIO 可能根本没写:
1 | gpio_write(red_led.pin, true); |
一句话先懂:信息隐藏不是故意藏代码,而是防止外部绕过接口,让软件状态和硬件状态脱节。
先看问题:字段被直接改,硬件不会自动跟着变
嵌入式里很多字段不是普通变量,它们背后对应真实硬件。
| 状态 | 结果 |
|---|---|
is_on == true |
软件以为灯亮了 |
| GPIO 没有同步写入 | 硬件实际没亮 |
后续逻辑依赖 is_on |
判断全部偏掉 |
调试时最麻烦的就是这种问题:日志看起来是对的,现场硬件行为却不对。
头文件只说外部能做什么
C 模块通常由 .h 和 .c 配合:
| 文件 | 职责 |
|---|---|
.h |
对外承诺:别人能调用哪些接口 |
.c |
内部实现:具体怎么操作硬件、怎么维护状态 |
LED 模块的头文件可以这样写:
1 |
|
这里为了教学方便,结构体仍放在头文件里。但规则要讲清楚:外部可以声明 Led_t 对象,不能直接修改成员,状态变化必须走 led_xxx() 接口。
更严格的工程写法是不透明结构体,也就是头文件只写:
1 | typedef struct Led Led_t; |
然后把结构体定义藏在 .c 里。本系列先用可见结构体降低理解成本,后面再逐步接近工业写法。
static 函数相当于模块内部 private
如果 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 修饰函数后,这个函数只在当前 .c 文件可见。外部模块不能调用它,也就不会依赖你的内部细节。
getter:允许读,但要受控地读
外部确实可能需要知道 LED 状态,比如上报日志、显示 UI、做故障判断。此时不要直接读多个字段,而是提供查询接口:
1 | int led_get_state(const Led_t *me, bool *is_on, uint8_t *brightness) |
这个签名有三个细节:
| 写法 | 作用 |
|---|---|
const Led_t *me |
表示只读对象,不修改状态 |
输出指针允许为 NULL |
调用者可以只取关心的值 |
| 返回错误码 | 参数非法时能明确失败 |
调用方式:
1 | bool is_on; |
新人常见误区
| 误区 | 为什么危险 |
|---|---|
| 结构体成员公开了就随便改 | 可能绕过硬件同步和合法性检查 |
static 只是为了避免重名 |
更重要的是限制内部函数的可见范围 |
| getter 没必要,直接读更快 | 直接读会把内部数据结构变成外部依赖 |
.h 放越多越方便 |
暴露越多,后续越难改实现 |
写模块时可以问自己一句:这个符号是“外部必须知道”,还是“内部自己用就行”?后者优先放进 .c,并加 static。
反面写法和推荐写法
| 操作 | 不推荐 | 推荐 |
|---|---|---|
| 点亮 LED | led.is_on = true |
led_on(&led) |
| 熄灭 LED | led.is_on = false |
led_off(&led) |
| 查询状态 | 直接读多个字段 | led_get_state() |
| 更新硬件 | 到处写 gpio_write() |
集中到 static helper |
| 内部辅助 | 暴露在头文件 | 放 .c 并加 static |
C/C++ 对照
| C 语言 | C++ |
|---|---|
.h 中声明的函数 |
public 方法 |
.c 中的 static 函数 |
private 方法 |
led_get_state() |
getter |
| 不直接改成员 | 访问控制 |
| 统一内部 helper | 私有工具函数 |
C 语言没有 private 关键字,但 static 能让函数和文件级变量仅在当前翻译单元可见。这是 C 模块化最重要的边界工具之一。
嵌入式项目意义
信息隐藏不是为了“防同事”,而是为了降低整个团队犯错的概率。
| 项目问题 | 信息隐藏带来的改善 |
|---|---|
| 软件状态和 GPIO 不一致 | 状态变化集中在接口里 |
| 多处直接操作寄存器 | 硬件改版只改内部 helper |
| 内部函数被外部依赖 | static 阻止依赖泄漏 |
| 查询和修改混在一起 | getter 让读写边界清楚 |
总结
.h只告诉外部能做什么,.c决定内部怎么做;状态变化必须通过接口,硬件更新必须集中管理。










