05 数据归位:你的全局变量,该死了

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

全局变量的问题,不是”看起来不优雅”。真正的问题是:它让数据没有主人。数据没有主人,谁都能改;谁都能改,就很难知道 bug 是谁带来的。

看一眼反面写法

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);  /* 红灯 */
bad_led_init(6); /* 绿灯 */
bad_led_on(); /* 你以为点红灯,实际 g_pin 已经是 6 */

第二次初始化把 g_pin 覆盖了。这类 bug 很隐蔽 — 编译正常,函数能调用,变量有值,日志看似合理,但硬件行为偏偏不对。

数据归位:每一份数据都要有主人

先把数据分成三类:

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

判断标准很简单:如果这个数据跟着”某一个对象”走,就放进对象;如果它属于”整个模块”,就放进 .c 并加 static;如果它不该变,就加 const

实例数据进 struct

正确写法:

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 文件可见。外部不能这样乱改:

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;

这是变量,不是常量。别人可以 MAX_BRIGHTNESS = 999;

正确写法:

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;
}

static 的三种含义

static 在 C 语言里有三种常见用法:

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

前两种是模块化设计的常用工具。第三种不是不能用,但会让函数内部有记忆,测试和调试都会更复杂。

嵌入式项目意义

嵌入式系统里的”全局变量满天飞”会带来很多实际问题:多实例互相覆盖导致多路外设无法独立工作,外部随便 extern 让模块边界失效,运行时改常量破坏参数约束,隐藏状态太多导致测试复现困难。

数据归位后,代码会更容易推理 — led_on(&red) 这行代码只应该影响 red,不应该偷偷影响 green


我见过的一个坑:同事休假期间,有人往 g_pin 写了新值,返工后 debug 了三天才发现是多实例覆盖。数据没有主人,bug 就是主人。