14、静态测试与动态测试:PC-Lint/Cppcheck静态分析、与单元测试的互补

做嵌入式开发这么多年,我越来越觉得一个道理:测试不是万能的,但没有测试是万万不能的。但很多人有个误区——以为写了单元测试就万事大吉了。其实不然。

今天咱们聊聊静态测试动态测试这对“黄金搭档”。说白了,静态测试是“不运行代码”的检查,动态测试是“跑起来”的验证。两者互补,缺一不可。

14.1 静态测试:代码的“体检医生”

静态测试,就是不执行程序,直接对源代码进行分析。它像个体检医生,能发现你代码里的“亚健康”状态。

我习惯把静态测试分成两类:

  • 代码规范检查:检查命名风格、缩进、注释等
  • 静态分析:检查潜在的逻辑错误、内存泄漏、空指针等

常用的工具有PC-Lint、Cppcheck、Clang Static Analyzer等。我个人用得最多的是Cppcheck,因为它开源、轻量,而且对嵌入式C代码支持很好。

核心观点:静态测试能在编译之前发现问题,成本最低,效率最高。

14.2 动态测试:代码的“路试员”

动态测试,就是让代码跑起来,观察它的行为是否符合预期。单元测试就是典型的动态测试。

动态测试能发现静态测试发现不了的问题:

  • 运行时逻辑错误
  • 边界条件处理
  • 时序问题
  • 资源竞争

但动态测试有个硬伤——覆盖率永远到不了100%。你想想看,代码路径组合是天文数字,你不可能把所有情况都测到。

14.3 静态 vs 动态:到底谁更厉害?

这个问题我经常被问到。我的回答是:别争了,两个都要

对比维度 静态测试 动态测试
是否需要运行 不需要 需要
发现问题类型 语法、风格、潜在缺陷 运行时逻辑、行为错误
覆盖率 理论上100%代码路径 受限于测试用例
误报率 较高(需要人工确认) 较低(实际运行结果)
执行速度 快(秒级) 慢(取决于测试用例数)
典型工具 PC-Lint, Cppcheck CUnit, Unity, Google Test

你看,两者各有千秋。静态测试快但可能误报,动态测试准但覆盖不全。所以,最佳实践是:先用静态测试扫一遍,再用动态测试重点验证

14.4 PC-Lint vs Cppcheck:我该怎么选?

这两个工具我都用过,说说我的感受。

PC-Lint

老牌工具,功能强大,规则极其丰富。但有个问题——配置复杂。我记得第一次用PC-Lint时,光配置文件就折腾了半天。而且它是商业软件,价格不菲。

Cppcheck

开源免费,上手快。虽然规则没PC-Lint那么多,但对嵌入式C的常见问题覆盖得很好。比如数组越界、空指针、未初始化变量等,它都能抓到。

我的建议:如果公司预算充足,用PC-Lint做深度分析;如果个人或小团队,Cppcheck完全够用。我现在的项目都是两者结合——Cppcheck做日常检查,PC-Lint做发布前的最终审查。

14.5 实战:用Cppcheck做静态分析

咱们看个例子。假设你有这么一段代码:

#include <stdio.h>
#include <stdlib.h>

void process_data(int *data, int size) {
    int i;
    int *buffer = (int*)malloc(size * sizeof(int));
    
    for (i = 0; i <= size; i++) {  // 这里有问题!
        buffer[i] = data[i] * 2;
    }
    
    // 忘记free了!
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    process_data(arr, 5);
    return 0;
}

这段代码有两个明显的问题:

  1. for循环条件写成了i <= size,数组越界了
  2. malloc分配的内存没有释放

用Cppcheck跑一下:

$ cppcheck --enable=all test.c
Checking test.c ...
test.c:8: error: Array 'data[5]' accessed at index 5, which is out of bounds.
test.c:7: error: Memory leak: buffer

看到了吗?两个问题都被抓出来了。而且Cppcheck还会给出具体的行号,方便定位。

注意:Cppcheck默认只检查错误,加上--enable=all才会检查警告和风格问题。我建议在CI流程中开启所有检查,然后人工过滤误报。

14.6 静态测试与单元测试的互补关系

这里我要强调一个观点:静态测试和单元测试不是替代关系,而是互补关系

我曾经在一个项目中吃过亏。当时团队只做单元测试,觉得覆盖率到了90%就稳了。结果上线后,一个空指针导致系统崩溃。后来一查,那个空指针在静态测试中早就被标记出来了,但没人看。

从那以后,我定了个规矩:

  • 编码阶段:用Cppcheck做静态检查,确保基本质量
  • 测试阶段:用单元测试验证功能逻辑
  • 发布前:用PC-Lint做深度分析,查漏补缺

说白了,静态测试帮你扫雷,单元测试帮你验证。两者结合,才能把bug扼杀在摇篮里。

14.7 知识体系:静态与动态测试的协作流程

下面这张图展示了静态测试和动态测试在开发流程中的协作关系:

静态测试与动态测试协作流程 编码阶段 编写代码 静态测试 Cppcheck / PC-Lint 发现问题?返回修改 动态测试 单元测试框架 测试失败?返回静态分析 集成测试 系统级验证 发布前审查 PC-Lint深度分析 上线运行 持续监控 静态测试:不运行代码,发现潜在缺陷 动态测试:运行代码,验证功能正确性 两者互补,缺一不可

14.8 避坑指南:我踩过的那些坑

最后分享几个我亲身经历过的教训:

  • 别迷信静态测试:它只能发现“已知模式”的问题。逻辑错误、业务规则错误,它发现不了。
  • 别忽视误报:我曾经因为Cppcheck报了一个“可能的内存泄漏”就改了代码,结果引入了真正的bug。后来我学会了:先确认,再修改
  • 静态测试要集成到CI:手动跑一次两次还行,时间长了就忘了。我建议把Cppcheck加到CI流水线里,每次提交代码自动检查。
  • 单元测试不能替代静态测试:反过来也一样。两者是互补的,不是替代的。

总结一句话:静态测试帮你发现“代码写得对不对”,动态测试帮你验证“功能做得对不对”。两个都做,才是真正的质量保障。


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