代码坏味道识别:如何发现嵌入式C代码中的“坏味道”
各位工程师朋友,咱们开始第二讲。上一章我们聊了模块化设计的顶层思路,这一章得先干点“脏活累活”——学会识别代码里的坏味道。
你可能要问:什么是坏味道?说白了,就是代码里那些“看着就不对劲”的地方。它不会让程序立刻崩溃,但会慢慢拖垮你的维护效率。我在项目里见过太多这样的例子:一个功能看起来跑得挺好,可一旦要加新需求,改一处崩三处。嗯,这就是坏味道在作祟。
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 坏味道的根源:为什么我们会写出这样的代码?
识别坏味道只是第一步。要想从根本上避免,还得理解它为什么会出现。我个人总结下来,主要有三个原因:
- 时间压力:项目赶工期,先跑起来再说。结果“先跑起来”的代码,一跑就是三年。
- 缺乏设计:上来就写代码,没有先画模块图、定义接口。写到后面发现耦合太紧,想拆也拆不动了。
- “能跑就行”的心态:这是最要命的。觉得代码只要功能正确,结构好不好无所谓。但你要知道,代码是写给人看的,顺便给机器执行。
2.4 本章知识体系:坏味道识别框架
下面这张图,是我在实际项目中总结的坏味道识别框架。它把三大坏味道的识别方法、典型特征和重构方向串在了一起。你可以把它当作一个检查清单来用。
2.5 本章小结
这一章我们聊了三个最常见的代码坏味道:过长函数、全局变量泛滥、重复代码。它们就像代码里的“慢性病”,不会立刻要命,但会慢慢侵蚀你的项目健康度。
记住几个关键点:
- 过长函数——看到超过50行、多个逻辑块,就拆。
- 全局变量泛滥——能用局部变量就别用全局,能用模块内部变量就别暴露到外部。
- 重复代码——出现三次以上,就提取成公共函数。