05 数据组织:实例状态、模块状态与常量
05 数据组织:实例状态、模块状态与常量
C 语言面向对象系列
一个模块中可能同时存在多种数据:每颗 LED 的状态、整个模块的初始化次数,以及固定的参数上限。它们的使用范围不同,存放方式也应有所区别。
多个实例共用全局变量的问题
如果把 LED 数据全部放在全局变量中:
1 | int g_pin = 0; |
初始化函数就会反复修改同一份数据:
1 | int bad_led_init(uint8_t pin) |
依次初始化两颗 LED:
1 | bad_led_init(5); /* 红灯 */ |
第二次调用将 g_pin 从 5 改为 6。后续操作只能取得最后写入的引脚号,无法区分红灯和绿灯。代码可能正常编译,日志中的变量也有值,但操作的设备已经发生变化。
我曾遇到过类似问题:同事休假期间,有人修改了 g_pin,返工后调试了三天,才发现是多实例共用数据造成的覆盖。
按使用范围组织数据
可以先分清三种情况:
| 数据 | 存放方式 | 示例 |
|---|---|---|
| 每个对象各有一份 | 结构体成员 | pin、brightness、is_on |
| 整个模块共享一份 | 源文件中的 static 变量 |
s_init_count、s_debug_flag |
| 模块内部使用的固定值 | static const 对象 |
MAX_BRIGHTNESS、MAX_PIN |
先确定数据属于单个实例还是整个模块,再决定它是否允许修改。
将实例数据放进结构体
引脚、亮度、开关状态和初始化标志都属于某一颗 LED:
1 | typedef struct { |
初始化函数通过 me 写入当前对象的成员:
1 | int led_init(Led_t *me, uint8_t pin) |
红灯和绿灯分别创建对象后,就能各自保存配置和状态:
1 | Led_t red; |
调用 led_on(&red) 时,函数读取的是 red 的数据;调用 led_on(&green) 时,读取的是 green 的数据。
用 static 保存模块共享状态
初始化次数、模块调试开关等数据可能需要在整个模块内共享:
1 | static int s_init_count = 0; |
文件作用域的 static 变量具有内部链接。其他源文件不能通过同名的 extern 声明访问这里的变量:
1 | extern int s_init_count; /* 不可行,static 变量没有外部链接 */ |
外部需要读取初始化次数时,可以提供查询函数:
1 | int led_get_init_count(void) |
这样既保留了模块内共享数据的能力,也将外部访问集中到明确的接口中。
用 static const 表达固定参数
下面的名称虽然使用了大写字母,定义的仍是可修改变量:
1 | int MAX_BRIGHTNESS = 255; |
代码仍然可以执行 MAX_BRIGHTNESS = 999;。如果值在运行期间不应改变,可以将其定义为 const。下面的示例采用 0~100 的亮度范围、0~15 的引脚范围:
1 | static const uint8_t MAX_BRIGHTNESS = 100; |
其中,static 限定链接范围,const 表示对象不应被修改,uint8_t 明确数据类型。具名常量也便于在源码和调试信息中识别参数,不过调试时是否保留名称还取决于编译设置。
在接口中使用参数约束
将常量、模块状态和内部检查函数放在一起后,接口可以统一验证参数:
1 | static const uint8_t MAX_BRIGHTNESS = 100; |
这里按顺序检查对象指针、初始化状态和亮度范围。检查通过后,只更新 me 指向的对象及对应 GPIO。与前面的 GPIO 示例相同,亮度为零时关闭,非零时开启;实际 PWM 调光在后续章节讨论。
区分 static 的使用位置
本系列涉及的三种常见用法如下:
| 使用位置 | 主要作用 | 使用时的考虑 |
|---|---|---|
| 文件作用域的函数 | 使函数具有内部链接 | 适合内部辅助函数 |
| 文件作用域的变量 | 使变量具有内部链接 | 适合模块共享状态 |
| 函数内的局部变量 | 在函数调用结束后保留值 | 需要考虑多次调用之间的状态依赖 |
前两种用法主要帮助组织模块。第三种会让后一次调用依赖前一次留下的数据,测试和调试时需要考虑这种影响。
实例数据分开保存,可以避免多个外设互相覆盖;模块数据通过接口访问,可以减少外部直接修改;固定参数明确为常量,可以保留参数约束。数据的修改范围清楚后,出现异常时也更容易定位。










