代码坏味道识别:如何发现嵌入式C代码中的“坏味道”

各位工程师朋友,咱们开始第二讲。上一章我们聊了模块化设计的顶层思路,这一章得先干点“脏活累活”——学会识别代码里的坏味道。

你可能要问:什么是坏味道?说白了,就是代码里那些“看着就不对劲”的地方。它不会让程序立刻崩溃,但会慢慢拖垮你的维护效率。我在项目里见过太多这样的例子:一个功能看起来跑得挺好,可一旦要加新需求,改一处崩三处。嗯,这就是坏味道在作祟。

核心观点:坏味道不是Bug,但它是Bug的温床。识别坏味道,是重构的第一步,也是最重要的一步。

2.1 三大典型坏味道:过长函数、全局变量泛滥、重复代码

嵌入式C代码里,坏味道的种类不少。但根据我的经验,过长函数、全局变量泛滥、重复代码这三类,占了实际项目中80%以上的重构工作量。咱们一个一个来看。

2.1.1 过长函数:一个函数干完所有事

什么叫过长函数?不是单纯看行数。我个人习惯是:如果一个函数需要你滚动屏幕才能看完,或者它的逻辑需要你画流程图才能理清,那它就太长了。

来看一个典型的例子。这是我曾经在一个车载诊断项目里遇到的真实代码(当然,我做了简化):

// 坏味道示例:过长函数
void process_sensor_data(void)
{
    uint8_t i;
    uint16_t raw_value;
    float calibrated_value;
    uint8_t status;
    
    // 步骤1:读取所有传感器原始数据
    for (i = 0; i < SENSOR_COUNT; i++)
    {
        raw_value = read_adc_channel(i);
        sensor_raw[i] = raw_value;
    }
    
    // 步骤2:校准数据(温度补偿+线性化)
    for (i = 0; i < SENSOR_COUNT; i++)
    {
        calibrated_value = (float)sensor_raw[i] * CALIB_COEFF;
        if (system_temperature > TEMP_THRESHOLD)
        {
            calibrated_value *= temp_comp_table[i];
        }
        sensor_calibrated[i] = calibrated_value;
    }
    
    // 步骤3:故障检测
    for (i = 0; i < SENSOR_COUNT; i++)
    {
        if (sensor_calibrated[i] > FAULT_HIGH_LIMIT)
        {
            status = FAULT_HIGH;
            set_fault_flag(i, status);
        }
        else if (sensor_calibrated[i] < FAULT_LOW_LIMIT)
        {
            status = FAULT_LOW;
            set_fault_flag(i, status);
        }
        else
        {
            status = NORMAL;
            clear_fault_flag(i);
        }
    }
    
    // 步骤4:计算平均值并上报
    // ... 还有50行
}

这个函数干了四件事:读取、校准、故障检测、上报。每一件事都值得单独成一个函数。你想想看,如果以后要改校准算法,你得在这个大函数里找到对应的那一段,还得小心别碰坏其他逻辑。我曾经就因为这样改出过一个Bug——改故障阈值的时候,不小心把校准系数也改了。

识别技巧:如果一个函数内部有超过两个独立的“段落”(用空行分隔的逻辑块),或者有超过两个不同层次的缩进,就该考虑拆分了。

2.1.2 全局变量泛滥:谁都能改,谁都说不清

嵌入式开发里,全局变量是个绕不开的话题。但“泛滥”就是另一回事了。我见过最夸张的一个项目,全局变量定义文件有800多行,各种flag、buffer、状态变量混在一起。每次出问题,排查流程都是:先搜这个变量在哪里被修改,然后发现改了10个地方,再然后……就没有然后了。

为什么会这样?因为全局变量破坏了模块之间的边界。任何函数都能读写它,你根本没法保证数据的一致性。

// 坏味道示例:全局变量泛滥
// global_vars.h
uint8_t system_mode;           // 系统模式
uint16_t adc_buffer[64];       // ADC缓冲区
uint8_t comm_rx_flag;          // 通信接收标志
uint8_t comm_tx_flag;          // 通信发送标志
uint32_t system_tick;          // 系统滴答
uint8_t fault_code;            // 故障码
uint16_t sensor_raw[8];        // 传感器原始值
float sensor_calibrated[8];    // 传感器校准值
// ... 还有几十个

你看,这些变量之间有没有关联?sensor_raw和sensor_calibrated应该属于传感器模块,comm_rx_flag和comm_tx_flag应该属于通信模块。把它们全堆在全局空间里,等于告诉所有人:“来吧,随便改!”

避坑指南:我曾经在一个项目中,因为全局变量system_mode被中断服务程序和主循环同时修改,导致系统在模式切换时出现随机死机。排查了整整两天,最后发现是中断里改了system_mode,而主循环读到的是不一致的值。从那以后,我给自己定了个规矩:中断和任务之间,绝对不用裸全局变量通信。

2.1.3 重复代码:Ctrl+C/V 的后遗症

重复代码可能是最容易被忽视的坏味道。因为它“能跑”,而且“看起来没什么问题”。但重复代码的隐患在于:当你需要修改一个逻辑时,你很可能只改了其中一处,而忘了其他几处。

来看一个典型的重复代码场景:

// 坏味道示例:重复代码
void process_channel_1(void)
{
    uint16_t raw = read_adc(1);
    float cal = (float)raw * 1.05f;
    if (cal > 100.0f) cal = 100.0f;
    set_output(1, (uint8_t)cal);
}

void process_channel_2(void)
{
    uint16_t raw = read_adc(2);
    float cal = (float)raw * 1.05f;
    if (cal > 100.0f) cal = 100.0f;
    set_output(2, (uint8_t)cal);
}

void process_channel_3(void)
{
    uint16_t raw = read_adc(3);
    float cal = (float)raw * 1.05f;
    if (cal > 100.0f) cal = 100.0f;
    set_output(3, (uint8_t)cal);
}

三个函数,除了通道号不同,逻辑完全一样。如果哪天要把校准系数从1.05改成1.08,你得改三个地方。漏一个,系统行为就不一致了。

识别技巧:在代码审查时,如果看到连续几段代码的结构完全一样,只是变量名或常量不同,那就是重复代码。用diff工具对比一下,一目了然。

2.2 坏味道识别清单与对比

为了方便你在实际项目中快速识别,我整理了一个对比表格。你可以把它打印出来,贴在工位上。

坏味道类型 典型特征 危害等级 常见场景
过长函数 超过50行;多个独立逻辑块;多层缩进 高 状态机处理、初始化流程、数据处理管线
全局变量泛滥 全局变量超过20个;跨模块直接访问;中断与任务共享 极高 系统配置、传感器数据池、通信缓冲区
重复代码 相似代码块出现3次以上;仅参数不同 中 多通道处理、协议解析、错误处理

2.3 坏味道的根源:为什么我们会写出这样的代码?

识别坏味道只是第一步。要想从根本上避免,还得理解它为什么会出现。我个人总结下来,主要有三个原因:

  • 时间压力:项目赶工期,先跑起来再说。结果“先跑起来”的代码,一跑就是三年。
  • 缺乏设计:上来就写代码,没有先画模块图、定义接口。写到后面发现耦合太紧,想拆也拆不动了。
  • “能跑就行”的心态:这是最要命的。觉得代码只要功能正确,结构好不好无所谓。但你要知道,代码是写给人看的,顺便给机器执行。
我的建议:在开始写任何功能代码之前,先花10分钟想清楚:这个函数应该做什么?它应该和哪些模块交互?数据流是怎样的?这10分钟,往往能省下后面10小时的调试时间。

2.4 本章知识体系:坏味道识别框架

下面这张图,是我在实际项目中总结的坏味道识别框架。它把三大坏味道的识别方法、典型特征和重构方向串在了一起。你可以把它当作一个检查清单来用。

嵌入式C代码坏味道识别框架 嵌入式C代码坏味道识别框架 代码坏味道 过长函数 全局变量泛滥 重复代码 识别方法 • 超过50行 • 多个独立逻辑块 识别方法 • 全局变量超过20个 • 跨模块直接访问 识别方法 • 相似代码出现3次以上 • 仅参数不同 重构方向:函数拆分 重构方向:模块封装 重构方向:提取公共函数 目标:可读、可维护、可测试

2.5 本章小结

这一章我们聊了三个最常见的代码坏味道:过长函数、全局变量泛滥、重复代码。它们就像代码里的“慢性病”,不会立刻要命,但会慢慢侵蚀你的项目健康度。

记住几个关键点:

  • 过长函数——看到超过50行、多个逻辑块,就拆。
  • 全局变量泛滥——能用局部变量就别用全局,能用模块内部变量就别暴露到外部。
  • 重复代码——出现三次以上,就提取成公共函数。