3-10 在问题出现之前就发现它们

d当你犯下语义错误时,该错误在运行程序时可能立即显现,也可能不会。某个问题可能长期潜伏在代码中未被察觉,直到新引入的代码或环境变化触发其表现为程序故障。错误在代码库中潜伏的时间越长,就越难被发现,原本简单的修复可能演变成耗费大量时间精力的调试之旅。

那么我们该如何应对呢?


避免犯错

最好的办法就是从一开始就避免犯错。以下是一些有助于规避错误的建议:

  • 遵循最佳实践。
  • 疲惫或沮丧时不要编程。休息片刻,稍后再继续。
  • 了解语言中的常见陷阱(那些我们反复告诫你不要做的事)。
  • 避免函数过长
  • 尽可能优先使用标准库而非自定义代码
  • 充分添加代码注释
  • 从简单方案入手,逐步增加复杂度
  • 规避花哨/非显而易见的解决方案
  • 优先优化可读性与可维护性,而非性能
"
Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?

—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)访问。

提示

常见推荐的静态分析工具包括:

免费工具:

多数工具提供扩展程序以实现IDE集成,例如Clang Power Tools扩展。

付费工具(开源项目可能免费):

posted @ 2026-02-12 20:40  游翔  阅读(1)  评论(0)    收藏  举报