跳转至

Debugging C++ Programs

我只会把自己感觉有些许陌生的内容整理一遍,很熟悉的内容就不写了,否则便失去了整理的意义

Basic Debug

1)通过注释与解注释判断问题可能出现在哪个代码段/函数调用 s - 良好的版本控制系统很重要 - 重复添加/删除或取消注释/注释调试语句的另一种方法是使用第三方库,该库允许您将调试语句保留在代码中,但通过预处理器宏以发布模式编译它们。 dbg就是这样一个仅包含头文件的库,它的存在是为了帮助实现这一点

2)使用std::cerr标识错误输出

当出于调试目的打印信息时,请使用 std::cerr 而不是 std::cout。原因之一是 std::cout ==可能==会被缓冲,这意味着在您要求 std::cout 输出信息和实际输出信息之间可能会有暂停。

如果您使用 std::cout 进行输出,然后程序立即崩溃,则 std::cout 可能尚未实际输出。这可能会误导您了解问题所在。另一方面,std::cerr 是无缓冲的,这意味着您发送给它的任何内容都会立即输出。这有助于确保所有调试输出尽快出现(以一些性能为代价,我们在调试时通常不关心这一点)。

举个例子加以说明:

C++
1
2
3
4
5
6
#include <iostream>

int main() {
    std::cout << "debug info for cout\n";
    // segmentation fault here
}
C++
1
2
3
4
5
6
#include <iostream>

int main() {
    std::cerr << "debug info for cerr\n";
    // segmentation fault here
}
  • 使用cout时,如果程序突然崩溃, 调试信息可能还留在缓冲区中, 根本看不到
  • 使用cerr时,即使程序崩溃, 调试信息也已经输出, 帮助定位问题

3)良好的调试习惯是将调试语句前置缩进,方便回头看

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

int getValue()
{
std::cerr << "getValue() called\n";
    return 4;
}

int main()
{
std::cerr << "main() called\n";
    std::cout << getValue << '\n';

    return 0;
}
  1. 添加临时调试语句时,建议不缩进它们。这使得以后更容易找到它们并将其删除。(当然,也可以使用#TODO(assert)之类的进行标识)
  2. 如果使用 clang-format 来格式化代码,它将尝试自动缩进这些行。可以像这样禁止自动格式化:
    C++
    1
    2
    3
    // clang-format off
    std::cerr << "main() called\n";
    // clang-format on
    

Advanced Debug

1)使用#define统一开关调试语句

我们举个例子:

C++
 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;
}

这里很直观,但是再一想,如果这是一个超级大的C++框架,我每次都得记住自己在哪里加了调试语句,然后再去注释掉,这样是不是很麻烦?

我们有没有更加简单的方法?如果能够统一开关它们的使用就好了!

我们可以使用宏定义来实现这个功能!

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

很显然,这个# define ENABLE_DEBUG应该安排在“文件控制器”的位置

2)Logger

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

日志文件有一些优点。由于写入日志文件的信息与程序的输出是分开的,因此您可以避免因混合正常输出和调试输出而造成的混乱。

这里logger有很多,不举例了,如果感兴趣可以看看ns-3里的:

C++
1
2
3
4
// define a logger
NS_LOG_COMPONENT_DEFINE("ComponentName");
// logging info
NS_LOG_INFO("Enter CWR recovery mode; cwnd=" << m_tcb->m_cWnd);

3)使用IDE调试器

强烈推荐,参考 chapter 3.6/3.7/3.8/3.9 and video recommended by CMU.

建议找个例子自己练练,越用越熟,其实很有趣