3-10 在问题出现之前就发现它们
d当你犯下语义错误时,该错误在运行程序时可能立即显现,也可能不会。某个问题可能长期潜伏在代码中未被察觉,直到新引入的代码或环境变化触发其表现为程序故障。错误在代码库中潜伏的时间越长,就越难被发现,原本简单的修复可能演变成耗费大量时间精力的调试之旅。
那么我们该如何应对呢?
避免犯错
最好的办法就是从一开始就避免犯错。以下是一些有助于规避错误的建议:
- 遵循最佳实践。
- 疲惫或沮丧时不要编程。休息片刻,稍后再继续。
- 了解语言中的常见陷阱(那些我们反复告诫你不要做的事)。
- 避免函数过长
- 尽可能优先使用标准库而非自定义代码
- 充分添加代码注释
- 从简单方案入手,逐步增加复杂度
- 规避花哨/非显而易见的解决方案
- 优先优化可读性与可维护性,而非性能
—Brian Kernighan, “The Elements of Programming Style”, 2nd edition
重构代码
当你在程序中添加新功能(即“行为变更”)时,会发现某些函数逐渐变长。函数越长,其复杂度越高且越难理解。
解决之道是将单个长函数拆分为多个短函数。这种在不改变程序行为的前提下调整代码结构的过程称为重构refactoring。重构的目标是通过增强程序的组织性与模块化程度来降低其复杂度。
那么函数多长算过长?通常占满一个屏幕高度的函数就被视为过长——若需滚动才能阅读完整函数,其可理解性将大幅下降。理想情况下,函数应控制在十行以内,而五行以内的函数则更佳。
请谨记:此处的目标是最大化可理解性与可维护性,而非追求函数长度最小化——为节省一两行代码而放弃最佳实践或使用晦涩编程技巧,对代码毫无裨益。
关键洞察
修改代码时,应先进行行为变更或结构变更,随后重新测试正确性。同时进行行为变更与结构变更往往会导致更多错误,且这些错误更难被发现。
防御性编程介绍
错误不仅可能源于自身(例如逻辑错误),也可能因用户以未预料的方式使用应用程序而产生。例如,当要求用户输入整数时,若用户输入字母,程序将如何应对?除非你预见到这种情况并添加相应错误处理,否则程序很可能无法正常运行。
防御性编程Defensive programming是一种实践方法,程序员通过预判软件可能遭遇的所有滥用场景来构建代码——无论是终端用户还是其他开发者(包括程序员自身)在使用代码时都可能引发滥用。这些滥用行为通常可被检测并加以缓解(例如提示输入错误的用户重新尝试)。
后续课程我们将深入探讨与错误处理相关的主题。
快速发现错误
由于在大型程序中完全避免错误很难实现,因此次优方案就是快速捕捉已出现的错误。
最佳做法是分阶段编写程序,每完成一小段代码就进行测试,确保其正常运行。
不过,我们还可以采用其他几种技术手段。
测试函数介绍
发现程序问题的常用方法之一是编写测试函数来“运行”你编写的代码。以下是一个简单的尝试,更多是为了说明目的:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
void testadd()
{
std::cout << "This function should print: 2 0 0 -2\n";
std::cout << add(1, 1) << ' ';
std::cout << add(-1, 1) << ' ';
std::cout << add(1, -1) << ' ';
std::cout << add(-1, -1) << ' ';
}
int main()
{
testadd();
return 0;
}
testadd() 函数通过传入不同参数调用 add() 函数来测试其行为。若所有测试结果均符合预期,则可合理确信该函数运作正常。更优的是,我们可以保留此测试函数,并在每次修改 add 函数时运行它,以确保未意外破坏其功能。
这属于单元测试unit testing的初级形式——该软件测试方法通过检验源代码的小单元来判断其正确性。
如同日志框架,现有多种第三方单元测试框架可供选用。虽然也可自行编写,但需借助更丰富的语言特性才能充分实现其功能。后续课程将对此进行深入探讨。
约束(constraints)介绍
基于约束的技术涉及添加额外代码(可在非调试构建中编译掉),用于检查是否违反了某些预设假设或预期。
例如,若编写计算数字阶乘的函数,该函数要求参数为非负数,则函数可在执行前检查调用方是否传入了非负数。若调用方传入负数,函数可立即报错而非返回不确定结果,从而确保问题能被即时发现。
实现此功能的常用方法是使用 assert 和 static_assert,相关内容将在第 9.6 课——Assert 与 static_assert 中详细讲解。
针对普遍问题的全面排查
程序员往往会犯某些常见错误,其中部分错误可通过专门训练的程序来发现。这类程序通常被称为静态分析工具static analysis tools(有时也非正式地称为代码检查工具linters),它们通过分析源代码来识别特定的语义问题(此处“静态”指这些工具在不执行代码的情况下进行分析)。静态分析工具发现的问题未必是当前具体故障的根源,但能帮助指出代码中的脆弱环节或特定情境下可能引发问题的隐患。
你其实已拥有一个现成的静态分析工具——编译器!除确保程序语法正确外,大多数现代C++编译器还会进行基础静态分析以识别常见问题。例如,若尝试使用未初始化的变量,多数编译器会发出警告。若尚未设置,可通过提升编译器警告与错误级别(参见第0.11课——编译器配置:警告与错误级别)来发现这些问题。
现存多种静态分析工具,其中部分工具可识别超过300种编程错误。在小型学术项目中,使用静态分析工具并非强制要求,但它能帮助你发现代码不符合最佳实践的环节。对于大型项目,强烈建议使用静态分析工具,它能揭示数十甚至数百个潜在问题。
最佳实践
使用静态分析工具检查程序,帮助发现代码不符合最佳实践的区域。
对于 Visual Studio 用户
Visual Studio 2019 及更高版本内置静态分析工具。可通过“构建” > “在解决方案上运行代码分析”(Alt+F11)访问。
提示
常见推荐的静态分析工具包括:
免费工具:
- clang-tidy
- cpplint
- cppcheck(已集成于Code::Blocks)
- SonarLint
多数工具提供扩展程序以实现IDE集成,例如Clang Power Tools扩展。
付费工具(开源项目可能免费):

浙公网安备 33010602011771号