Cpp学习5 第 3 章 C++程序调试

铁名_IronName Lv5

3.1 — 语法和语义错误

Syntax errors  语法错误

Semantic errors  语义错误

语义错误是指含义上的错误。当一条语句在语法上有效,但违反了语言的其他规则,或者没有达到程序员预期的效果时,就会出现语义错误。
在大多数复杂的程序中,运行时语义错误很难通过肉眼观察代码找到。这时,调试技巧就派上用场了。

3.2 — 调试过程

调试的一般方法

一旦发现问题,调试问题通常包括六个步骤:

  1. 找到问题的根本原因(通常是出错的代码行)。我们将在下一课中讨论一些方法。
  2. 请务必弄清楚问题发生的原因。
  3. 确定如何解决这个问题。
  4. 修复导致问题的原因。
  5. 重新测试以确保问题已解决。
  6. 重新测试以确保没有出现新问题。

3.3 — 调试策略

调试程序时,大多数情况下,你绝大部分时间都会花在查找错误所在。
随着程序变得越来越复杂,通过代码检查来发现问题也变得越来越复杂。

通过运行程序查找问题

观察程序运行时的行为,并尝试从中诊断问题。这种方法可以概括为:

  1. 找出重现问题的方法
  2. 运行程序并收集信息,以缩小问题范围。
  3. 重复上一步,直到找到问题为止。

当你排除了所有其他可能的原因后,剩下的就只有导致问题的那一行代码了。接下来,我们只需要弄清楚它为什么会这样。

3.4 — 基本调试技巧

调试技巧一:注释掉你的代码

尝试注释掉无关代码。

警告

别忘了你注释掉了哪些函数,以便以后可以取消注释!

在进行了许多与调试相关的更改之后,很容易忘记撤销一两项更改。如果发生这种情况,你最终会修复一个错误,但又会引入其他错误!

拥有一个好的版本控制系统在这里非常有用,因为你可以将你的代码与主分支进行比较,以查看你所做的所有与调试相关的更改(并确保在提交更改之前还原它们)。

另一种避免反复添加/删除或取消注释/注释调试语句的方法是使用第三方库,该库允许您在代码中保留调试语句,但在发布模式下通过预处理器宏将其编译掉。

调试技巧二:验证代码流程

更复杂的程序中常见的另一个问题是,程序调用函数的次数过多或过少(甚至根本没有调用)。
在这种情况下,在函数顶部添加语句来打印函数名会很有帮助。这样,程序运行时,你就可以看到哪些函数被调用了。

提示

在进行调试时,请使用 std::cerr 而不是 std::cout 。原因之一是 std::cout 可能使用缓冲,这意味着从你请求 std::cout 输出文本到它实际输出之间可能会有一段时间的延迟。如果你使用 std::cout 输出后程序立即崩溃,那么 std::cout 可能尚未实际输出。这可能会误导你,让你无法确定问题所在。另一方面, std::cerr 不使用缓冲,这意味着你发送给它的任何内容都会立即输出。这有助于确保所有调试输出尽快显示(但会牺牲一些性能,不过在调试时我们通常不需要考虑性能)。

使用 std::cerr 还可以明确地表明输出的信息是错误情况而不是正常情况。

添加临时调试语句时,最好不要缩进。这样便于以后查找和删除它们。

调试技巧三:打印值

为什么使用打印语句进行调试并不理想

虽然向程序中添加调试语句以进行诊断是一种常见的基本技术,也是一种有效的技术(尤其是在由于某种原因无法使用调试器时),但由于多种原因,它并不是一个好方法:

  1. 调试语句会使你的代码变得杂乱无章。
  2. 调试语句会使程序的输出变得杂乱无章。
  3. 调试语句需要修改代码以添加或删除语句,这可能会引入新的错误。
  4. 调试语句用完后必须删除,这使得它们无法重用。

3.5 — 更多调试技巧

对调试代码进行条件化

使用预处理器指令使调试语句具有条件性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

#define ENABLE_DEBUG // comment out to disable debugging

int getUserInput()
{
#ifdef ENABLE_DEBUG
std::cerr << "getUserInput() called\n";
#endif
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}

int main()
{
#ifdef ENABLE_DEBUG
std::cerr << "main() called\n";
#endif
int x{ getUserInput() };
std::cout << "You entered: " << x << '\n';

return 0;
}

现在,我们只需注释掉/取消注释 #define ENABLE_DEBUG 即可启用调试。这样,我们就可以重用之前添加的调试语句,并在使用完毕后禁用它们,而无需从代码中实际删除它们。如果这是一个多文件程序,#define ENABLE_DEBUG 会放在一个包含到所有代码文件的头文件中,这样我们就可以在单个位置注释掉/取消注释 #define,使其影响到所有代码文件。

使用日志记录器logger

除了通过预处理器进行条件调试之外,另一种方法是将调试信息发送到日志中。 日志是对已发生事件的顺序记录,通常带有时间戳。生成日志的过程称为日志记录 。通常,日志会写入磁盘上的文件(称为日志文件 ),以便稍后查看。大多数应用程序和操作系统都会写入日志文件,这些文件可用于帮助诊断出现的问题。

3.6 — 使用集成调试器:单步执行

调试器是一种计算机程序,它允许程序员控制另一个程序的执行方式,并在程序运行时检查其状态。例如,程序员可以使用调试器逐行执行程序,并检查沿途变量的值。通过比较变量的实际值和预期值,或者观察代码的执行路径,调试器可以极大地帮助程序员追踪语义(逻辑)错误。
虽然集成调试器非常方便,推荐给初学者,但命令行调试器也得到了很好的支持,并且在不支持图形界面的环境(例如嵌入式系统)中仍然很常用。

提示

不要忽视学习如何使用调试器。随着程序变得越来越复杂,你花在学习如何有效使用集成调试器上的时间,与你节省下来查找和修复问题的时间相比,简直微不足道。

Stepping  步进

单步执行(Stepping)是一组相关的调试器功能的名称,它允许我们逐条语句地执行(逐步执行)代码。
step into (逐语句)命令会执行程序正常执行路径中的下一条语句,然后暂停程序的执行,以便我们可以使用调试器检查程序的状态。如果正在执行的语句包含函数调用, step into 会使程序跳转到被调用函数的开头并暂停。

与 “单步执行 ”命令类似,“step over”(逐过程)命令也会执行程序正常执行路径中的下一条语句。但是, “step into” 会进入函数调用并逐行执行,而 “step over” 则会执行整个函数而不停止,并在函数执行完毕后将控制权返回给用户。

Step out(跳出) 执行当前正在执行的函数中所有剩余的代码,并在函数返回后将控制权交还给你。当你不小心进入了一个你不想调试的函数时,这个命令非常有用。

如果你越过了预期的目标,通常的做法是停止调试并重新开始调试,这次要更加小心,不要再越过目标。

3.7 — 使用集成调试器:运行和断点

第一个常用的命令是“ 运行到光标处  (Run to cursor)。该命令会执行程序,直到执行到光标选中的语句为止。然后,它会将控制权交还给你,以便你可以从该点开始调试。这使得你可以高效地从代码中的特定位置开始调试,或者在调试过程中直接跳转到想要进一步检查的地方
在调试过程中,你可能希望从当前代码点继续运行程序。最简单的方法是使用 continue 命令。continue debug 命令会让程序像往常一样继续运行,直到程序终止,或者直到某些事件触发控制权返回给你(例如断点,我们将在本课后面部分介绍)。
在 Visual Studio 中调试程序时,可以通过 “调试”菜单 >“继续” 或按 F5 快捷键来访问 继续 命令。

continue 命令有一个对应 的 命令,名为 startstart 命令执行与 continue 相同的操作,只是它会从程序开头重新开始执行。start 命令只能在非调试会话中调用。(也就是 “开始调试” )

断点是一种特殊的标记,它告诉调试器在调试模式下运行时,程序在断点处停止执行。

set next statement

还有一个调试命令虽然不太常用,但即使不常用也值得了解一下。set next statement 命令允许我们将执行点更改为其他语句(有时也非正式地称为_跳转_ )。这可以用来向前跳转执行点,跳过一些原本会执行的代码;也可以向后跳转执行点,让已经执行过的代码再次运行。

在 Visual Studio 中,您可以通过右键单击语句并从上下文菜单中选择 “设置下一条语句” 或按 Ctrl+Shift+F10 快捷键组合来跳转到代码执行点。此选项具有上下文关联性,仅在调试程序时可用。

警告

设置下一个语句 命令会改变程序的执行位置,但不会改变程序的其他状态。变量将保留跳转前的值。因此,跳转可能会导致程序产生与预期不同的值、结果或行为。请谨慎使用此功能(尤其是向后跳转)。
不要把执行点更改为其他函数,这可能导致未定义行为,甚至导致程序崩溃。

3.8 — 使用集成调试器:监视变量

单步执行程序只是调试器功能的一半。调试器还允许你在单步执行代码的同时检查变量的值,而无需修改代码。
请确保 项目 是使用 Debug 构建配置编译的,如果使用的是 Release 配置编译项目,调试器的功能可能无法正常工作。

监视窗口

所有现代集成调试器都提供了一个名为“监视窗口”的功能。 监视窗口允许您添加需要持续监控的变量,这些变量会在您单步执行程序时更新。进入调试模式时,监视窗口可能已经显示在屏幕上;如果没有,您可以通过 IDE 的窗口命令将其调出。

实践

在 Visual Studio 中,监视窗口位于 “调试”菜单 > 窗口 > 监视 > 监视 1 中 。请注意,必须处于调试模式才能启用此选项,因此请先 单步执行 程序。
此窗口的显示位置(左侧、右侧或底部)可能有所不同。可以通过将 “Watch 1” 标签拖动到应用程序窗口的另一侧来更改其停靠位置。

在监视变量上设置断点

实践

在 Visual Studio 中,确保你的变量已被监视。接下来, 单步 执行程序并转到监视窗口。右键单击该变量,然后选择“值更改时中断”。
每次启动调试会话时,都需要重新启用“值更改时中断”。

监视窗口还可以计算简单的表达式。可以在监视窗口中输入 x + 2

本地监视

因为在调试过程中检查函数内部局部变量的值很常见,所以许多调试器都会提供一些方法来快速查看作用域内 所有 局部变量的值。

实践

在 Visual Studio 中,可以在 “局部变量” 窗口中查看所有局部变量的值,该窗口位于 “调试”菜单 > 窗口 > 局部变量 。请注意,必须处于调试会话中才能激活此窗口。
(默认开启的)

3.9 — 使用集成调试器:调用堆栈

调用栈是所有已调用函数的列表,这些函数共同构成了当前执行点。调用栈包含每个已调用函数的条目,以及函数返回时将要执行的代码行。每当调用一个新函数时,该函数都会被添加到调用栈的顶部。当当前函数返回给调用者时,它会从调用栈顶部移除,控制权返回到它下方的函数。
调用堆栈窗口是一个调试器窗口,用于显示当前的调用堆栈。如果您看不到调用堆栈窗口,则需要告诉 IDE 显示它。

调用堆栈与断点结合使用非常有用,当断点被触发时,你想知道为了到达代码中的特定位置而调用了哪些函数。

3.10 — 在问题出现之前发现它们

避免犯错

最好的办法当然是避免犯错。以下是一些有助于避免犯错的方法:

  • 遵循最佳实践。
  • 不要在疲惫或沮丧时编程。休息一下,稍后再回来。
  • 了解语言学习中常见的陷阱(所有我们警告你不要做的事情)。
  • 不要让你的函数写得太长。
  • 尽可能优先使用标准库,而不是自己编写代码。
  • 请为你的代码添加大量注释。
  • 先从简单的解决方案入手,然后逐步增加复杂性。
  • 避免使用巧妙/不合常理的解决方案。
  • 优先考虑可读性和可维护性,而不是性能。

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 your code 重构你的代码

随着您为程序添加新功能(“行为改变”),您会发现某些函数的篇幅会增加。函数越长,就越复杂,也越难理解。
解决这个问题的一个方法是将一个冗长的函数拆分成多个较短的函数。这种在不改变代码行为的前提下对代码结构进行修改的过程称为重构 。重构的目标是通过提高代码的组织性和模块化程度来降低程序的复杂度。

那么,函数多长才算太长呢?通常来说,占据一屏垂直空间的代码就被认为太长了——如果需要滚动屏幕才能阅读完整函数,那么函数的可读性就会显著下降。理想情况下,函数应该少于十行。少于五行的函数就更好了。
(这里的目标是最大限度地提高代码的可理解性和可维护性,而不是最大限度地缩短函数长度——为了节省一两行代码而放弃最佳实践或使用晦涩的编码技术,对你的代码没有任何好处。)

修改代码时,要么只修改行为,要么只修改结构,然后重新测试其正确性。同时修改行为和结构往往会导致更多错误,而且这些错误更难发现。

防御性编程入门

防御性编程是一种实践,程序员会尝试预测软件可能被滥用的所有方式,无论是最终用户还是其他开发人员(包括程序员自己)在使用代码时可能造成的滥用。这些滥用行为通常可以被检测到并加以缓解(例如,要求输入错误信息的用户重试)。
我们将在以后的课程中探讨与错误处理相关的主题。

测试函数

发现程序问题的常用方法之一是编写测试函数来“运行”你编写的代码。

testadd() 函数通过使用不同的值来调用 add() 函数,从而测试 add() 函数。如果所有值都符合我们的预期,那么我们就可以相当肯定该函数能够正常工作。更妙的是,我们可以保留这个函数,并在每次修改 add 函数时运行它,以确保我们没有意外地破坏它。
这是一种原始的单元测试形式,单元测试是一种软件测试方法,通过这种方法测试源代码的小单元,以确定它们是否正确。

约束简介

基于约束的技术涉及添加一些额外的代码(如果需要,可以在非调试版本中编译掉),以检查是否违反了某些假设或预期。
例如,如果我们编写一个计算某个数的阶乘的函数,该函数需要一个非负参数,那么在继续执行之前,函数可以检查调用者是否传入了一个非负数。如果调用者传入了一个负数,那么函数可以立即报错,而不是产生不确定的结果,从而确保问题能够被及时发现。
实现此目的的一种常见方法是通过 assert 和 static_assert ,我们将在第 9.6 课中介绍——断言和 static_assert 。

程序员往往会犯一些常见的错误,而有些错误可以通过经过专门训练的程序来发现。这些程序通常被称为静态分析工具 (有时也非正式地称为_代码检查器_ ),它们会分析源代码以识别特定的语义问题(在此上下文中, “静态” 指的是这些工具在不执行代码的情况下进行分析)。静态分析工具发现的问题可能并非您遇到的具体问题的根源,但它们可以帮助您指出代码中存在的薄弱环节或在某些情况下可能引发问题的潜在风险。

Visual Studio 2019 及更高版本都内置了静态分析工具。您可以通过 “生成”>“运行解决方案代码分析”(Alt+F11) 来访问它。

3.x — 第三章总结和测验,pass.

  • 标题: Cpp学习5 第 3 章 C++程序调试
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-07 11:31:38
  • 更新于 : 2026-08-08 09:45:35
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习5/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论