第28章 代码审查与测试:审查清单、测试覆盖率审查、常见代码缺陷

代码审查这事儿,我做了十几年了。说实话,刚入行那会儿我觉得审查就是浪费时间——自己写的代码自己最清楚,干嘛还要别人看?直到有一次,我提交了一个看似完美的模块,结果被同事揪出一个极其隐蔽的指针越界问题。嗯,从那以后,我再也不敢小看代码审查了。

这一章,咱们就聊聊代码审查和测试覆盖率审查。我会结合自己的经验,给你一份实用的审查清单,再讲讲那些常见的代码缺陷。你想想看,如果能把这些东西内化成习惯,你的代码质量至少能提升一个档次。

28.1 代码审查清单:我自己的“防呆”手册

我个人习惯把审查清单分成几个维度。每次审查代码时,我都会在心里过一遍这张清单。说白了,这就是一份“防呆”手册,防止自己漏掉关键点。

28.1.1 逻辑与算法审查

这是最核心的部分。代码能不能跑对,全看逻辑。

  • 边界条件:数组的第一个和最后一个元素处理了吗?循环的起始和终止条件对吗?
  • 空值处理:指针是否为NULL?字符串是否为空?结构体是否已初始化?
  • 溢出风险:整数运算会不会溢出?我记得有一次,一个计数器在极端情况下溢出了,导致系统死循环。从那以后,我对所有累加操作都格外小心。
  • 状态机完整性:所有状态都有对应的处理吗?有没有遗漏的状态转换?

核心原则:永远假设输入是恶意的。哪怕是你自己写的函数,也要假设调用者会传进来各种奇怪的值。

28.1.2 内存与资源管理审查

嵌入式开发中,内存问题是最常见的“隐形杀手”。

  • malloc/free 配对:每次分配都有对应的释放吗?有没有内存泄漏?
  • 野指针:释放后有没有把指针置为NULL?我曾经见过一个项目,因为野指针导致系统随机崩溃,排查了整整三天。
  • 栈溢出:局部变量是不是太大了?递归深度有没有控制?
  • 资源泄漏:文件句柄、锁、信号量,这些资源用完后有没有归还?

避坑指南:我曾经在审查一个通信协议栈时,发现某个错误处理路径下忘记释放锁。结果那个锁一旦被占用,整个系统就死锁了。所以,我建议你在写错误处理代码时,先把资源释放逻辑写出来,再写正常逻辑。

28.1.3 并发与同步审查

多线程或多任务环境下,问题往往出在“时机”上。

  • 临界区保护:共享变量有没有加锁?锁的粒度是否合理?
  • 死锁风险:锁的获取顺序是否一致?有没有嵌套锁?
  • 竞态条件:检查-使用模式(check-then-act)是否安全?
  • 原子操作:对于简单的读写,是否可以用原子操作代替锁?

28.2 测试覆盖率审查:别被数字骗了

很多团队把“测试覆盖率”当成KPI,觉得只要覆盖率超过80%就万事大吉。其实不然。我见过一个项目,覆盖率高达90%,但核心逻辑的边界条件根本没测到。

测试覆盖率审查,说白了就是检查你的测试到底测了什么、没测什么。

28.2.1 语句覆盖率 vs. 分支覆盖率

这两个概念经常被混淆。我简单解释一下:

覆盖率类型 含义 常见误区
语句覆盖率 每行代码是否被执行过 容易达到100%,但意义有限
分支覆盖率 每个if-else分支是否都被覆盖 比语句覆盖率更有价值
条件覆盖率 每个布尔子表达式是否都取过true/false 最严格,也最难达到
MC/DC覆盖率 每个条件独立影响结果 航空、汽车等安全关键领域要求

我个人习惯,至少要求分支覆盖率超过85%。对于关键模块,我会要求MC/DC覆盖率。

28.2.2 审查测试用例的“盲区”

审查覆盖率报告时,我会重点关注几个地方:

  • 异常路径:错误处理代码有没有被测试覆盖?很多测试只测“快乐路径”,忽略了异常情况。
  • 边界值:比如一个函数接受0-100的参数,测试用例有没有包含0、100、-1、101?
  • 时序相关:并发场景下的测试是否充分?有没有模拟过超时、中断等场景?

小技巧:我通常会在审查时,手动在覆盖率报告上标记出“未覆盖的代码行”。然后问自己:这些代码真的不需要测吗?还是测试用例遗漏了?

28.3 常见代码缺陷:我踩过的那些坑

下面这些缺陷,是我在多年审查中反复遇到的。你想想看,如果能提前避开这些坑,能省下多少调试时间。

28.3.1 整数溢出与符号问题

这是C语言的老大难问题。我见过一个案例:一个温度传感器读取值,用int8_t存储,结果温度超过127度后直接变成负数,导致系统误判为“低温报警”。

// 错误示例:整数溢出
int16_t calculate_average(int16_t *data, uint8_t len) {
    int16_t sum = 0;
    for (uint8_t i = 0; i < len; i++) {
        sum += data[i];  // 如果len很大,sum可能溢出
    }
    return sum / len;
}

// 改进:使用更大的类型,或检查溢出
int32_t calculate_average_safe(int16_t *data, uint8_t len) {
    int32_t sum = 0;
    for (uint8_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum / len;
}

28.3.2 缓冲区溢出

这个缺陷在嵌入式开发中尤其致命。因为嵌入式系统通常没有MMU保护,缓冲区溢出直接破坏栈或堆,导致各种诡异的问题。

// 错误示例:未检查长度
void copy_data(char *dest, const char *src) {
    strcpy(dest, src);  // 如果src比dest长,就溢出了
}

// 改进:使用带长度限制的函数
void copy_data_safe(char *dest, size_t dest_size, const char *src) {
    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0';
}

避坑指南:我曾经审查过一个网络协议栈,里面用了sprintf拼接日志字符串。结果某个异常情况下,日志内容超长,直接覆盖了相邻的变量。从那以后,我要求团队一律使用snprintf。

28.3.3 未初始化变量

这个缺陷很隐蔽,因为局部变量在栈上的初始值是不确定的。不同编译器、不同优化级别下,行为可能完全不同。

// 错误示例:未初始化
int process_data(void) {
    int result;  // 未初始化
    if (some_condition) {
        result = 100;
    }
    // 如果some_condition为false,result的值是未定义的
    return result;
}

// 改进:总是初始化
int process_data_safe(void) {
    int result = 0;  // 明确初始化
    if (some_condition) {
        result = 100;
    }
    return result;
}

28.3.4 逻辑短路与副作用

C语言的逻辑运算符(&&、||)有短路特性。如果滥用,会导致意想不到的行为。

// 错误示例:依赖副作用的短路
if (ptr != NULL && *ptr == 10) {
    // 没问题,短路保护了
}

// 危险示例:函数调用有副作用
if (func_a() || func_b()) {
    // 如果func_a返回true,func_b不会被执行
    // 如果func_b有副作用(比如修改全局变量),就出问题了
}

28.4 知识体系:代码审查与测试的核心逻辑

下面这张图,是我自己总结的代码审查与测试的知识体系。你可以把它当作一张“地图”,随时对照检查。

代码审查与测试 审查清单 测试覆盖率审查 常见代码缺陷 逻辑与算法 内存与资源 并发与同步 语句覆盖率 分支覆盖率 MC/DC覆盖率 整数溢出 缓冲区溢出 未初始化变量 逻辑短路 核心目标:发现缺陷、提升质量、积累经验 审查不是找茬,而是团队共同成长的过程

28.5 我的审查流程:从“看代码”到“读代码”

最后,分享一下我个人的审查流程。说白了,就是怎么把“看代码”变成“读代码”。

  1. 先看整体结构:不急着看细节。先理解模块的职责、接口、数据流。
  2. 再走读关键路径:跟着主要逻辑走一遍,确认流程是否正确。
  3. 然后检查边界:重点关注边界条件、异常处理、错误路径。
  4. 最后审查细节:变量命名、注释、代码风格等。

个人经验:我每次审查时,都会在代码旁边写注释。不是写“这里错了”,而是写“这里为什么这样写?”。有时候,写着写着就发现问题了。这个方法,我推荐给所有团队成员。

代码审查不是走过场,而是实实在在的质量保障手段。你想想看,如果能通过审查提前发现90%的缺陷,那调试阶段能省下多少时间?


公众号:蓝海资料掘金营,微信deep3321