3-5 更多调试策略(todo)
在上节课(3.4——基础调试技巧)中,我们开始探讨如何手动调试问题。该节课对使用语句打印调试文本提出了一些批评:
- 调试语句会使代码杂乱无章。
- 调试语句会污染程序输出结果。
- 添加或移除调试语句都需要修改代码,可能引入新错误。
- 调试语句在完成调试后必须删除,导致其无法复用。
我们可通过某些基础技术缓解这些问题。本节课将探讨具体实现方法。
对调试代码进行条件化处理
请考虑以下包含调试语句的程序:
#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;
}

完成调试语句的使用后,您需要将其删除或注释掉。若后续需要重新启用,则需重新添加或取消注释。
为方便在程序中灵活启用/禁用调试功能,可通过预处理指令将调试语句设置为条件语句:
#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应置于所有代码文件均需包含的头文件中。这样只需在单一位置注释/取消注释该宏定义,即可使变更传播至所有代码文件。
此方案解决了删除调试语句的繁琐操作及潜在风险,但代价是代码更显冗杂。此方案的另一缺点在于:若出现拼写错误(如“DEBUG”拼写错误)或忘记在代码文件中包含头文件,该文件的部分或全部调试功能可能无法启用。因此尽管优于无条件版本,仍有改进空间。
使用日志记录器
除通过预处理器实现条件化调试外,另一种方法是将调试信息发送到日志。日志log是对已发生事件的顺序记录,通常带有时间戳。生成日志的过程称为日志记录logging。通常,日志会被写入磁盘文件(称为日志文件log file)以便后续查阅。大多数应用程序和操作系统都会生成日志文件,这些文件可用于诊断发生的问题。
日志文件具有若干优势:由于写入日志的信息与程序输出分离,可避免正常输出与调试输出混杂造成的混乱。日志文件还便于发送给他人进行诊断——当用户使用软件时遇到问题,可要求其发送日志文件,这有助于定位问题根源。
C++内置名为std::clog的输出流,专用于记录日志信息。但默认情况下,std::clog会写入标准错误流(与std::cerr相同)。虽然可将其重定向至文件,但在该领域通常更推荐使用现成的第三方日志工具。具体选用哪款工具由您决定。
为便于说明,我们将演示使用plog日志器输出日志的效果。plog以头文件集形式实现,可轻松包含在任何需要的位置,且轻量易用。
#include <plog/Log.h> // Step 1: include the logger headers
#include <plog/Initializers/RollingFileInitializer.h>
#include <iostream>
int getUserInput()
{
PLOGD << "getUserInput() called"; // PLOGD is defined by the plog library
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}
int main()
{
plog::init(plog::debug, "Logfile.txt"); // Step 2: initialize the logger
PLOGD << "main() called"; // Step 3: Output to the log as if you were writing to the console
int x{ getUserInput() };
std::cout << "You entered: " << x << '\n';
return 0;
}
以下是上述记录器生成的输出(位于Logfile.txt文件中):
2018-12-26 20:03:33.295 DEBUG [4752] [main@19] main() called
2018-12-26 20:03:33.296 DEBUG [4752] [getUserInput@7] getUserInput() called
如何包含、初始化及使用日志器将因您选择的具体日志器而异。
请注意,采用此方法时也不需要条件编译指令,因为大多数日志器都提供减少/消除日志输出的方法。这使得代码更易于阅读,因为条件编译语句会增加大量冗余。使用plog时,可通过将初始化语句修改为以下形式来临时禁用日志记录:
plog::init(plog::none , "Logfile.txt"); // plog::none eliminates writing of most messages, essentially turning logging off
我们今后不会在任何课程中使用plog这个词,所以你不必担心学习它。
顺便提一下……
若需自行编译上述示例或在项目中使用plog,可按以下步骤安装:
首先获取最新plog版本:
- 访问plog代码库
- 点击右上角绿色代码按钮,选择“下载zip文件”
随后将整个压缩包解压至硬盘任意位置
最后,针对每个项目,在IDE中将“某目录\plog-master\include\”设置为包含目录。Visual Studio操作指南详见此处:A.2 -- 使用Visual Studio与Code::Blocks集成库;Code::Blocks指南详见此处:A.3 -- 使用Code::Blocks集成库。由于plog不提供预编译库文件,可跳过相关步骤。
日志文件通常会生成在可执行文件所在的目录中。
提示
在大型或对性能敏感的项目中,可能需要更快速且功能更丰富的日志记录器,例如 spdlog。

浙公网安备 33010602011771号