03 信息隐藏:同事改一行,LED全乱了

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全貌

struct + me 把 LED 封装成对象之后,还不能停在这里。

如果结构体成员暴露在外,别人依然可以绕过接口直接改状态。这会带来一个嵌入式里很常见、也很恶心的问题:软件状态变了,硬件状态没变。

先看问题:直接改字段,硬件没跟着变

假设 LED 对象长这样:

1
2
3
4
5
typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
} Led_t;

正常点亮应该调用:

1
led_on(&red_led);

但有人图省事,直接写:

1
red_led.is_on = true;

这行代码让软件变量变成了”亮”,但并没有调用:

1
gpio_write(red_led.pin, true);

于是系统变成这样:

状态 结果
is_on == true 软件以为灯亮了
GPIO 没有写高 硬件实际没亮
后续逻辑依赖 is_on 判断全部偏掉

调试时最麻烦的就是这种问题:日志看起来是对的,现场硬件行为却不对。

一句话先懂:信息隐藏不是故意藏代码,而是防止外部绕过接口,让软件状态和硬件状态脱节。

.h 是菜单,.c 是后厨

一个 C 模块通常由头文件和源文件组成:

文件 类比 应该放什么
.h 菜单 外部能调用的接口
.c 后厨 具体实现、辅助函数、内部状态

外部只应该通过菜单点菜,不应该冲进后厨自己翻锅。

LED 模块的头文件可以这样设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef LED_H
#define LED_H

#include <stdbool.h>
#include <stdint.h>

typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
} Led_t;

int led_init(Led_t *me, uint8_t pin);
int led_on(Led_t *me);
int led_off(Led_t *me);
int led_set_brightness(Led_t *me, uint8_t brightness);
int led_get_state(const Led_t *me, bool *is_on, uint8_t *brightness);

#endif

这里虽然为了教学还把 struct 放在头文件里,但约束非常明确:外部通过 led_xxx() 接口操作对象,不直接改成员。更严格的工程写法是不透明结构体,后面项目复杂时可以再升级。

static 函数:C 语言里的 private

如果 GPIO 写操作散落在多个函数里:

1
2
3
4
5
6
7
8
9
10
11
12
13
int led_on(Led_t *me)
{
me->is_on = true;
gpio_write(me->pin, true);
return 0;
}

int led_off(Led_t *me)
{
me->is_on = false;
gpio_write(me->pin, false);
return 0;
}

一旦硬件改成低电平点亮,就要到处改。

更好的做法是集中到一个内部函数:

1
2
3
4
static void led_update_hardware(Led_t *me)
{
gpio_write(me->pin, me->is_on);
}

然后所有公开接口都调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int led_on(Led_t *me)
{
if (me == NULL) {
return -1;
}

me->is_on = true;
led_update_hardware(me);

return 0;
}

int led_off(Led_t *me)
{
if (me == NULL) {
return -1;
}

me->is_on = false;
led_update_hardware(me);

return 0;
}

static 修饰函数时,含义是:这个函数只在当前 .c 文件内部可见。外部文件不能调用 led_update_hardware(),只能调用 led_on()led_off()

getter:安全读取,不直接访问

外部想知道 LED 状态怎么办?不要直接读字段,而是提供查询接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int led_get_state(const Led_t *me, bool *is_on, uint8_t *brightness)
{
if (me == NULL) {
return -1;
}

if (is_on != NULL) {
*is_on = me->is_on;
}

if (brightness != NULL) {
*brightness = me->brightness;
}

return 0;
}

这里有三个细节:

写法 作用
const Led_t *me 表示只读对象,不修改状态
is_on != NULL 调用者可以只取自己关心的值
返回错误码 参数非法时明确失败

调用者这样使用:

1
2
3
4
5
6
bool is_on;
uint8_t brightness;

if (led_get_state(&red_led, &is_on, &brightness) == 0) {
/* 根据状态做业务判断 */
}

反面写法和正确写法

场景 不推荐 推荐
点亮 LED led.is_on = true led_on(&led)
熄灭 LED led.is_on = false led_off(&led)
查询状态 直接读多个字段 led_get_state()
更新硬件 到处写 gpio_write() 集中到 static 函数
内部辅助 暴露在头文件 .c 并加 static

C/C++ 对照

C 语言 C++
.h 中声明的函数 public 方法
.c 中的 static 函数 private 方法
led_get_state() getter
不直接改成员 访问控制
统一内部 helper 私有工具函数

C 语言没有 private 关键字,但 static 能让函数和文件级变量仅在本文件可见。这就是 C 语言模块化的基础边界。

嵌入式项目意义

嵌入式代码不是只改变量。很多变量背后对应真实硬件。如果外部直接改状态,就可能出现:

问题 后果
软件状态和 GPIO 不一致 现场行为和日志对不上
多处直接操作寄存器 硬件改版时难维护
内部 helper 被外部调用 模块边界被打穿
查询和修改混在一起 调试时无法判断是谁改了状态

信息隐藏的目标不是”防别人”,而是降低所有人犯错的概率。


下次你直接改 led.is_on 之前,问自己一句:硬件会跟着变吗?