章节目录

更多调试策略

本节阅读量:

在上一课中,我们开始探索如何手动调试问题。在课程末尾,我们指出了使用打印语句进行调试的一些缺点:

  1. 调试语句扰乱了代码
  2. 调试语句扰乱了输出内容
  3. 调试语句需要不断增加和移除代码,容易引入新的问题
  4. 在解决问题之后需要移除调试代码,而这些代码完全不可重用

本节介绍的技术可以缓解其中一些问题。


条件化调试代码

考虑以下包含一些调试语句的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
 
int getUserInput()
{
std::cerr << "getUserInput() called\n";
	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}
 
int main()
{
std::cerr << "main() called\n";
    int x{ getUserInput() };
    std::cout << "You entered: " << x << '\n';
 
    return 0;
}

完成调试后,你要么需要删除这些调试语句,要么将它们注释掉。如果以后还需要用到,则必须重新添加它们,或取消注释。

利用预处理指令,可以很方便地在整个程序中启用或禁用调试语句。

 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 // 注释掉这一行,可以禁用掉调试输出

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,并将其效果传播到所有代码文件。

这种方法解决了必须删除调试语句以及误删代码的风险,但代价是代码变得更加混乱。它的另一个缺点是,如果你输入了错误(例如拼写错了”DEBUG”)或忘记将头文件包含在代码文件中,那么对应的文件就不会有调试语句输出。因此,尽管这比之前的版本更好,但仍有改进的余地。


使用日志记录器(Logger)

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

日志文件有几个优点。由于写入日志文件的信息与程序的正常输出是分离的,因此可以避免混合正常输出和调试输出所导致的混乱。日志文件也可以很容易地发送给其他人进行诊断——因此,如果使用你的软件的人遇到了问题,你可以要求他们将日志文件发送给你,这可能有助于提供问题所在的线索。

C++包含一个名为std::clog的输出流,用于写入日志信息。然而,默认情况下,std::clog写入的是标准错误流(与std::cerr相同)。虽然你可以将其重定向到文件,但通常更好的做法是使用现有的第三方日志库。

为了便于说明,我们将展示如何使用plog库。Plog以一组头文件的形式实现,因此很容易将其包含在你需要的任何位置,而且它是轻量级的,易于使用。

 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 <plog/Log.h> // Step 1: 引用对应的头文件
#include <plog/Initializers/RollingFileInitializer.h>
#include <iostream>

int getUserInput()
{
	PLOGD << "getUserInput() called"; // PLOGD 在 plog 对应的头文件库中定义

	std::cout << "Enter a number: ";
	int x{};
	std::cin >> x;
	return x;
}

int main()
{
	plog::init(plog::debug, "Logfile.txt"); // Step 2: 初始化plog库,指定对应的输出文件

	PLOGD << "main() called"; // Step 3: 将所有需要记录的信息写入日志文件中

	int x{ getUserInput() };
	std::cout << "You entered: " << x << '\n';

	return 0;
}

以下是来自上述代码的输出(在Logfile.txt文件中):

1
2
2018-12-26 20:03:33.295 DEBUG [4752] [main@14] main() called
2018-12-26 20:03:33.296 DEBUG [4752] [getUserInput@4] getUserInput() called

具体如何引用、初始化和使用日志记录器,将因你选择的库而异。

请注意,使用这种方法也不需要条件编译指令,因为大多数日志库都提供了相应的方法来控制日志的写入输出。这使得代码更容易阅读,因为条件编译行会增加许多混乱。使用plog时,可以通过将init语句更改为以下内容来临时禁用日志记录:

1
	plog::init(plog::none , "Logfile.txt"); // plog::none 关闭写入日志功能

我们在后续的课程中不会使用plog,因此你不需要担心如何学习它。


3.3 基本调试策略

上一节

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

下一节