当你犯了一个语义错误时,这个错误在你运行程序时可能不会立即被注意到。一个问题可能会在你的代码中潜伏很长时间,直到新引入的代码或情况变化导致它表现为程序故障。一个错误在代码库中存在的时间越长,就越难找到,而且本来可能很容易修复的问题会变成一场耗费时间和精力的调试冒险。
那么我们能做些什么呢?
不要犯错
嗯,最好的办法是首先不要犯错。这里列出了一些可以帮助避免犯错的事情
- 遵循最佳实践。
- 不要在疲倦或沮丧时编程。休息一下,稍后再回来。
- 了解语言中常见的陷阱(所有我们警告你不要做的事情)。
- 不要让你的函数太长。
- 尽可能优先使用标准库,而不是自己编写代码。
- 大量注释你的代码。
- 从简单的解决方案开始,然后逐步增加复杂性。
- 避免使用巧妙/不明显的解决方案。
- 优化可读性和可维护性,而不是性能。
“每个人都知道调试比第一次编写程序难两倍。所以如果你在编写时已经尽可能聪明了,那你将如何调试它呢?”
重构你的代码
当你为程序添加新功能(“行为变更”)时,你会发现有些函数会变长。随着函数变长,它们会变得更复杂,也更难理解。
解决这个问题的一种方法是将一个长的函数拆分成多个短的函数。这种在不改变代码行为的情况下对代码进行结构性更改的过程称为重构。重构的目标是通过增加程序的组织性和模块化来降低其复杂性。
那么,函数多长才算太长呢?一个占据一整屏代码的函数通常被认为太长——如果你需要滚动才能读完整个函数,那么函数的理解度会显著下降。理想情况下,函数应该少于十行。少于五行的函数甚至更好。
请记住,这里的目标是最大限度地提高理解性和可维护性,而不是最小化函数长度——放弃最佳实践或使用晦涩的编码技术来节省一两行代码并不会对你的代码有任何好处。
关键见解
在更改代码时,要么进行行为更改,要么进行结构更改,然后重新测试其正确性。同时进行行为和结构更改往往会导致更多的错误,并且这些错误更难发现。
防御性编程简介
错误不仅可能由你自己造成(例如,不正确的逻辑),也可能在用户以你未曾预料的方式使用应用程序时发生。例如,如果你要求用户输入一个整数,而他们输入了一个字母,你的程序在这种情况下会如何表现?除非你预料到这种情况并为此添加了一些错误处理,否则结果可能不会很好。
防御性编程是一种实践,程序员试图预测软件可能被滥用的所有方式,无论是最终用户还是使用代码的其他开发人员(包括程序员自己)。这些滥用通常可以被检测到并加以缓解(例如,通过要求输入错误的用户再次尝试)。
我们将在未来的课程中探讨与错误处理相关的主题。
快速发现错误
由于在大型程序中不犯错很难,其次最好的方法是快速捕获你所犯的错误。
最好的方法是每次编写少量代码,然后测试你的代码并确保它能正常工作。
然而,我们还可以使用其他一些技术。
测试函数简介
一种常见的揭示程序问题的方法是编写测试函数来“操练”你编写的代码。这是一个初步尝试,更多是为了说明目的而不是其他
#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` 函数时运行它,以确保我们没有意外地破坏它。
这是一种原始形式的单元测试,它是一种软件测试方法,通过测试源代码的小单元来确定它们是否正确。
与日志框架一样,有许多第三方单元测试框架可以使用。也可以自己编写,尽管我们需要更多的语言特性才能充分讨论这个主题。我们将在未来的课程中再次讨论其中的一些内容。
约束简介
基于约束的技术涉及添加一些额外的代码(如果需要,可以在非调试构建中编译掉),以检查某些假设或期望是否未被违反。
例如,如果我们正在编写一个函数来计算一个数字的阶乘,该函数期望一个非负参数,那么该函数可以在继续之前检查调用者是否传入了一个非负数。如果调用者传入一个负数,那么该函数可以立即出错,而不是产生一些不确定的结果,从而有助于确保问题立即被捕获。
一种常用的方法是通过 `assert` 和 `static_assert` 来实现,我们将在 9.6 课 -- assert 和 static_assert 中介绍它们。
全面排查一般性问题
程序员倾向于犯某些常见的错误,其中一些错误可以通过专门寻找它们的程序来发现。这些程序通常被称为静态分析工具(有时非正式地称为代码检查器),它们分析你的源代码以识别特定的语义问题(在这种情况下,静态意味着这些工具在不执行源代码的情况下进行分析)。静态分析工具发现的问题可能与你正在遇到的任何特定问题有关,也可能无关,但它们可能有助于指出代码的脆弱区域或在某些情况下可能出现问题的地方。
你已经有一个静态分析工具可供使用——你的编译器!除了确保你的程序在语法上是正确的,大多数现代 C++ 编译器都会进行一些轻量级的静态分析,以识别一些常见问题。例如,如果你尝试使用一个未初始化的变量,许多编译器会发出警告。如果你还没有这样做,提高你的编译器警告和错误级别(参见课程0.11 -- 配置你的编译器:警告和错误级别)可以帮助发现这些问题。
存在许多静态分析工具,其中一些可以识别超过 300 种编程错误。在我们的小型学术程序中,使用静态分析工具是可选的,但使用它可以帮助你找到代码中不符合最佳实践的地方。在大型程序中,强烈建议使用静态分析工具,因为它可以发现数十或数百个潜在问题。
最佳实践
在你的程序中使用静态分析工具,帮助查找代码中不符合最佳实践的地方。
对于 Visual Studio 用户
Visual Studio 2019 及更高版本自带内置静态分析工具。你可以通过“生成 > 对解决方案运行代码分析 (Alt+F11)”来访问它。
提示
一些常用的静态分析工具推荐包括
免费
- clang-tidy
- cpplint
- cppcheck(已集成到 Code::Blocks 中)
- SonarLint
其中大多数都有扩展,允许它们集成到你的 IDE 中。例如,Clang Power Tools 扩展。
付费(开源项目可能免费)