05 数据归位:struct 成员与 static 变量

C 语言 OOP 系列导航

本系列面向会 C 和单片机基础、刚进入企业项目的同学,从 struct 封装一路讲到 ops、多态和类 Linux 驱动框架。

  1. C 语言面向对象设计路线
  2. 封装:struct 数据与 me 指针
  3. 信息隐藏:static 与头文件边界
  4. 类的组织:前缀、init/deinit 与生命周期
  5. 数据归位:struct 成员与 static 变量 ⇐ 当前位置
  6. HAL 映射:工业库的封装结构
  7. 继承:struct 嵌套复用公共字段
  8. 函数指针:把行为作为参数
  9. ops/vtable:行为表与对象绑定
  10. 多态与转型:base 指针与 dispatch
  11. 接口设计:ops 合同与默认实现
  12. 完整框架:抽象层与硬件解耦
  13. 终章:自动注册与 Linux 内核 OOP 结构

很多新人听到“不要用全局变量”,会以为这是代码风格问题。其实更准确地说:不要让数据失去主人。

下面这种写法把 LED 模块的所有数据都放在全局:

1
2
3
4
5
int g_pin = 0;
int g_brightness = 0;
int init_count = 0;
int MAX_BRIGHTNESS = 255;
int g_debug_flag = 0;

初始化函数依赖这些全局变量:

1
2
3
4
5
6
7
8
9
10
11
int bad_led_init(uint8_t pin)
{
g_pin = pin;
g_brightness = 0;
init_count++;

gpio_init(pin);
gpio_write(pin, false);

return 0;
}

调用两次后,问题就出现了:

1
2
3
bad_led_init(5);  /* 初始化第一颗 LED */
bad_led_init(6); /* 初始化第二颗 LED,g_pin 被覆盖 */
bad_led_on(); /* 实际操作的是 pin 6 */

一句话先懂:对象自己的数据进 struct,模块共享的数据加 static,不该被修改的数据加 const

先看问题:变量有值,不代表值属于当前对象

全局变量最容易制造这种错觉:代码能编译,函数能调用,变量也有值,但它已经被别的对象覆盖了。

表面现象 实际问题
编译正常 数据归属错了
函数能调用 操作对象错了
变量有值 值已经被别的对象覆盖
日志看似合理 硬件行为不对

所以判断一个变量该放哪里,不要先问“哪里写起来方便”,而要问“它属于谁”。

数据按归属分类

数据类型 应该放哪里 示例
每个对象各有一份 struct 成员 pinbrightnessis_on
整个模块共享一份 .c 文件里的 static 变量 s_init_counts_debug_flag
不应该被修改 static const MAX_BRIGHTNESSMAX_PIN

这张表是写 C 模块时很实用的判断标准。变量放对位置,后面很多 bug 自然消失。

实例数据进入 struct

每颗 LED 自己的数据放进对象本身:

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

初始化时写入对象自己的字段:

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

me->pin = pin;
me->brightness = 0;
me->is_on = false;
me->initialized = true;

gpio_init(pin);
gpio_write(pin, false);

return 0;
}

红灯和绿灯互不覆盖:

1
2
3
4
5
6
7
8
Led_t red;
Led_t green;

led_init(&red, 5);
led_init(&green, 6);

led_on(&red); /* 操作 pin 5 */
led_on(&green); /* 操作 pin 6 */

模块共享数据用 static

有些数据确实不属于某一颗 LED,而属于整个 LED 模块。例如初始化次数、调试开关:

1
2
static int s_init_count = 0;
static int s_debug_flag = 0;

static 修饰文件作用域变量时,表示这份变量只在当前 .c 文件可见。外部无法用 extern 直接拿到它:

1
extern int s_init_count; /* 链接失败:static 变量没有外部链接 */

如果外部只需要读取,就提供查询接口:

1
2
3
4
int led_get_init_count(void)
{
return s_init_count;
}

这样模块仍然是这份数据唯一的写入方。

常量用 static const

下面这种写法只是名字像常量,实际上仍然能被改:

1
int MAX_BRIGHTNESS = 255;

更稳的写法:

1
2
static const uint8_t MAX_BRIGHTNESS = 100;
static const uint8_t MAX_PIN = 15;
写法 作用
static 只在当前文件可见
const 不允许修改
uint8_t 类型明确
保留变量名 调试时比宏更友好

完整片段

三类数据放好后,代码会变得更容易推理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static const uint8_t MAX_BRIGHTNESS = 100;
static const uint8_t MAX_PIN = 15;

static int s_init_count = 0;
static int s_debug_flag = 0;

static bool led_is_pin_valid(uint8_t pin)
{
return pin <= MAX_PIN;
}

static bool led_is_brightness_valid(uint8_t brightness)
{
return brightness <= MAX_BRIGHTNESS;
}

int led_set_brightness(Led_t *me, uint8_t brightness)
{
if (me == NULL) {
return -1;
}

if (!me->initialized) {
return -2;
}

if (!led_is_brightness_valid(brightness)) {
return -3;
}

me->brightness = brightness;
me->is_on = (brightness > 0);
gpio_write(me->pin, me->is_on);

return 0;
}

led_set_brightness(&red, 80) 只应该影响 red,不应该偷偷影响 green。这就是数据归位的价值。

static 的三种常见用法

用法 含义 建议
修饰函数 当前文件私有函数 推荐,用来隐藏 helper
修饰全局变量 当前文件私有变量 推荐,用来保存模块状态
修饰局部变量 函数结束后值仍保留 谨慎,容易制造隐藏状态

前两种是模块化设计常用工具;第三种会让函数内部带着跨调用状态,测试和复现都更麻烦。

新人常见误区

误区 更稳的做法
所有模块状态都用 g_ 全局变量 先判断属于对象还是模块
static 变量更安全,所以随便用 它只是限制可见性,不解决归属判断
常量用大写变量名就行 static const 明确不可修改
为了省参数,把当前对象存全局 多实例时会立刻互相覆盖

C/C++ 对照

C 写法 C++ 写法
me->pin this->pin
static int s_init_count static int Led::init_count
static const uint8_t MAX static const / constexpr
static bool helper() private 方法
led_get_init_count() 静态 getter

嵌入式项目意义

嵌入式系统里的“全局变量满天飞”会带来实际问题:

问题 影响
多实例互相覆盖 多路外设无法独立工作
外部随便 extern 模块边界失效
运行时改常量 参数约束被破坏
隐藏状态太多 测试复现困难

总结

对象自己的数据进 struct,模块共享的数据加 static,不该改的数据加 const;数据没有主人,bug 就是主人。