第4章:变量作用域前缀——g_、s_、m_与无前缀的江湖规矩

说实话,变量命名这件事,我见过太多团队在这上面栽跟头了。

有一次,一个同事调试了整整三天,最后发现是全局变量和局部变量重名了。编译器没报错,但逻辑全乱了。嗯,从那以后,我们团队就严格执行这套前缀规则。

4.1 全局变量:g_前缀

全局变量,说白了就是整个程序都能访问的变量。它的生命周期贯穿整个程序运行,谁都能改,谁都能读。

我个人习惯,全局变量必须加 g_ 前缀。为什么?

  • 一眼识别:看到 g_ 就知道这变量是全局的,修改时要三思
  • 避免冲突:局部变量和全局变量同名时,编译器会优先使用局部变量,这坑我踩过
  • 代码审查:review 时看到 g_ 前缀,会格外注意它的使用范围

正确示例:

// 全局变量定义
int g_system_tick = 0;
uint32_t g_adc_value = 0;
char g_device_name[32] = "sensor_01";

// 使用全局变量
void timer_isr(void) {
    g_system_tick++;
}

我曾经犯过的错:

在一个多线程项目中,我定义了一个全局变量 count,没加 g_ 前缀。结果某个线程里也定义了一个局部变量 count,两个变量互相干扰,数据全乱了。调试了两天才发现,从那以后我再也不敢省略 g_ 前缀了。

4.2 静态变量:s_前缀

静态变量分两种:文件内静态和函数内静态。它们的作用域都有限制,但生命周期是全局的。

我建议,静态变量统一加 s_ 前缀。你想想看,这样写代码的人一眼就知道:这个变量只在当前文件或当前函数内有效。

静态变量类型 作用域 生命周期 命名示例
文件内静态 当前文件 程序运行期间 s_file_status
函数内静态 当前函数 程序运行期间 s_call_count

实用技巧:

我在驱动开发中经常用静态变量来保存硬件状态。比如一个 UART 驱动,用 s_uart_tx_busy 来表示发送是否忙。这样其他文件想改也改不了,安全又清晰。

// 文件内静态变量
static int s_init_done = 0;

void module_init(void) {
    if (!s_init_done) {
        // 初始化代码
        s_init_done = 1;
    }
}

// 函数内静态变量
int get_call_count(void) {
    static int s_call_count = 0;
    return s_call_count++;
}

4.3 局部变量:不加前缀

局部变量,就是函数内部定义的临时变量。它的作用域只在当前函数内,生命周期随函数调用结束而结束。

说白了,局部变量不需要前缀。为什么?因为它的影响范围太小了,加了前缀反而显得啰嗦。

  • 简洁明了int i;int l_i; 清爽多了
  • 临时性:局部变量用完就丢,不需要长期维护
  • 可读性:函数内部代码本来就短,变量名短一点反而好读

正确写法:

void process_data(uint8_t *buffer, int len) {
    int i;           // 循环变量,不加前缀
    int sum = 0;     // 临时变量,不加前缀
    uint8_t temp;    // 临时变量,不加前缀
    
    for (i = 0; i < len; i++) {
        temp = buffer[i];
        sum += temp;
    }
}

注意:

局部变量虽然不加前缀,但名字也不能太随意。我见过有人写 int a, b, c, d;,这种代码过两天自己都看不懂。局部变量也要有语义,比如 indexcounterchecksum 这样的名字。

4.4 模块内部变量:m_前缀

模块内部变量,这个是我个人特别推崇的。它介于全局和局部之间,只在当前模块(通常是一个 .c 文件)内使用,但又不是静态的。

为什么需要 m_ 前缀?

  • 模块封装:告诉其他开发者,这个变量是模块内部使用的,别乱动
  • 代码分层:看到 m_ 就知道这是模块级变量,不是全局的
  • 便于重构:想改模块内部实现时,只关注 m_ 开头的变量就行
// sensor_module.c
// 模块内部变量
int m_sensor_status = 0;
float m_temperature = 25.0f;
uint32_t m_last_read_time = 0;

void sensor_init(void) {
    m_sensor_status = 1;
    m_temperature = 25.0f;
    m_last_read_time = 0;
}

int sensor_read(float *temp) {
    if (!m_sensor_status) {
        return -1;
    }
    // 读取传感器
    *temp = m_temperature;
    m_last_read_time = get_tick();
    return 0;
}

我的经验:

在 RTOS 项目中,我经常用 m_ 前缀来管理任务相关的变量。比如一个温度采集任务,用 m_temp_bufferm_sample_count 这样的变量,其他任务想访问?对不起,请通过接口函数。这样模块间的耦合度大大降低。

4.5 避坑指南

好了,说了这么多规则,我来总结几个实际开发中容易踩的坑:

我曾经踩过的坑:

  1. 全局变量不加 g_:在一个 10 万行的项目中,全局变量和局部变量重名,编译器不报错,但逻辑全乱。排查了整整两天。
  2. 静态变量忘记加 s_:写了一个中断服务函数,里面用了静态变量保存状态。结果另一个文件也定义了一个同名变量,两个变量互相覆盖,中断响应全乱了。
  3. 模块变量和全局变量混淆:一个模块的 m_ 变量被另一个模块直接访问,导致数据不一致。后来强制要求所有模块间通信必须通过接口函数。
  4. 局部变量名字太随意:写过 int a, b, c, d, e; 的代码,一个月后自己都看不懂。现在局部变量至少要有语义,比如 indexcountflag

4.6 总结

这套前缀规则,说白了就是让代码自己会说话。看到 g_ 知道是全局,看到 s_ 知道是静态,看到 m_ 知道是模块内部,没前缀的就是局部变量。

我个人觉得,命名规范这东西,越早统一越好。等代码写到几万行再想改,那真是欲哭无泪。你想想看,是不是这个理?

一句话记住:

全局加 g_,静态加 s_,模块加 m_,局部不加。简单粗暴,但管用。