diff --git "a/03DebugC++\347\250\213\345\272\217/3.1 \350\257\255\346\263\225\345\222\214\350\257\255\344\271\211\351\224\231\350\257\257.md" "b/03DebugC++\347\250\213\345\272\217/3.1 \350\257\255\346\263\225\345\222\214\350\257\255\344\271\211\351\224\231\350\257\257.md" new file mode 100644 index 0000000..40b6ceb --- /dev/null +++ "b/03DebugC++\347\250\213\345\272\217/3.1 \350\257\255\346\263\225\345\222\214\350\257\255\344\271\211\351\224\231\350\257\257.md" @@ -0,0 +1,89 @@ +# 语法和语义错误 + + + +软件错误是普遍存在的。制造它们很容易,而发现它们却很难。在本章中,我们将探讨查找和清楚C++程序内的错误有关的话题,包括学习如何使用集成开发环境中的集成调试器。 + +虽然调试工具和技术并不是C++标准的一部分,但是学会在你编写的程序中发现和消除错误是成为一个成功的程序员的极其重要的一部分。因此,我们会花一点时间来介绍这个主题,这样随着你编写的程序越来越复杂,你诊断和补救问题的能力也会提高。 + +如果你有使用其他编译语言调试程序的经验,那么其中的大部分内容你都会很熟悉。 + +# 语法和语义错误 + +编程是很有挑战性的,而且C++是一种有一些怪异的语言。把这两者放在一起,就会有很多方法来作死。错误一般分为两类:语法错误和语义错误(逻辑错误)。 + +当你写出的语句按照C++语法是无效的时候,就会出现`语法错误`,这包括缺少分号、使用未声明的变量、小括号和大括号不匹配等等错误。例如,下面的程序包含了不少语法错误 + +```c++ +#include + +int main() +{ + std::cout < "Hi there"; << x; // 无效的操作符(<),多余的分号,未定义的变量 + return 0 // 语句结尾缺少分号 +} +``` + +幸运的是,编译器一般会捕捉到语法错误,并产生警告和错误,所以你很容易发现并修复问题。然后只需要再编译一次,直到把所有的错误都解决掉。 + +一旦你的程序编译正确,让它实际产出你想要的结果可能有些棘手。当一条语句再语法上是有效的,但是却没有达到程序员的意图的时候,就会发生`语义错误`。 + +有时候,这些会导致你的程序崩溃,比如下面的程序在处以0的情况下 + +```c++ +#include + +int main() +{ + int a { 10 }; + int b { 0 }; + std::cout << a << " / " << b << " = " << a / b; // 除以0没有被定义 + return 0; +} +``` + +更多的时候,这些只会产生错误的值或者行为: + +```c++ +#include + +int main() +{ + int x; + std::cout << x; // 使用未初始化的变量会导致未定义的结果 + return 0; +} +``` + +或者 + +```c++ +#include + +int add(int x, int y) +{ + return x - y; // 函数要做的是加,写成了减 +} + +int main() +{ + std::cout << add(5, 3); // 预想的是8,结果是2 + return 0; +} +``` + +或者 + +```c++ +#include + +int main() +{ + return 0; // 函数在这个地方返回 + std::cout << "Hello,World!"; // 这条语句永远不会被执行 +} +``` + +现代编译器在检测某些常见语义错误(比如使用未初始化变量)方面已经做的越来越好。然而,在大多数情况下,编译器将无法发现大多数这类情况,因为编译器的设计是为了执行语法,而不是搞明白你的意图。 + +在上面的例子中,错误是相当容易发现的。但是在大多数程序中,语义错误并不容易通过目测代码来发现。这时候,调试技术就可以派上用场了。 \ No newline at end of file diff --git "a/03DebugC++\347\250\213\345\272\217/3.2 \350\260\203\350\257\225\346\265\201\347\250\213.md" "b/03DebugC++\347\250\213\345\272\217/3.2 \350\260\203\350\257\225\346\265\201\347\250\213.md" new file mode 100644 index 0000000..10b88f3 --- /dev/null +++ "b/03DebugC++\347\250\213\345\272\217/3.2 \350\260\203\350\257\225\346\265\201\347\250\213.md" @@ -0,0 +1,63 @@ +# 调试流程 + + + +比如说你写了一个程序,但是它不能正常工作 -- 代码能正常编译,但是当你运行它的时候,却得到了一个不正确的结果。你一定是在某个地方出现了语义错误。如何才能找到它呢?如果你一直在遵循最佳实践,写一点代码,然后进行测试,你可能已经知道你的错误在哪里了。或者呃逆可能完全没有头绪。 + +所有的bug都源自于一个简单的前提:一些你认为正确的东西,并不正确。实际上要找出这个错误在哪里是很有挑战性的。在本节中,我们将概述调试程序的一般流程。 + +因为我们还没有涉及到那么多的C++主题,所以我们这一节的示例程序会非常基础。这可能会使我们在这里展示的一些技术显得过度。但是,请记住,这些技术是为了用于大型的、更复杂的程序而设计的,在这样的环境中(这也是你最需要它们的地方)会有更大的用处。 + +# 调试的一般步骤 + +一旦发现问题,调试问题一般包括五个步骤: + +1. 找出问题的根本原因(通常是哪行代码无法工作) +2. 确保你了解问题发生的原因 +3. 确定你将如何解决这个问题 +4. 修复问题 +5. 重写测试确保问题已经被解决,没有出现新的问题 + +我们在这里用一个现实生活中的比喻。比如说,有一天晚上,你去冰箱里面的制冰机取冰。你把被子放到饮冰机下面,按了一下,然后....什么也没出来。呃,你发现了毛病,你会怎么做?你可能会开始调查,看看能否找出问题的根本原因。 + +`找出根本原因`:既然你听到了制冰机送冰的声音,很可能不是送冰装置本身的问题。于是你打开冰柜,检查冰盘。没有冰块。这就是问题的根本原因吗?不,这只是症状。经过进一步的检查,你确定制冰机似乎没有制冰。是制冰机的问题还是其他原因?冰箱仍然是冰的,水管也没有阻塞,其他一起似乎都在工作,所以你断定根本原因是制冰机不工作。 + +`了解问题`:在这个案例中,问题很简单,坏掉的制冰机不制冰 + +`确定修复方案` :在这一点上,你有几个选择来解决这个问题。你可以解决这个问题(从商店买冰袋)。你可以尝试进一步诊断制冰机,看看是否有可以维修的部件。你可以买一个新的制冰机,然后把它安装在现有的位置。或者你可以买个新的冰箱。 + +`修复问题`:一旦制冰机到了,你就把它安装好 + +`重新测试`: 重新接通电源并等待一夜后,你的新制冰机开始制冰,没有发现新的问题。 + +现在让我们把这个过程应用到上一节课的程序中 + +```c++ +#include + +// 将两个数字相加 +int add(int x, int y) +{ + return x - y; // 应该相加,却执行了减法 +} + +int main() +{ + std::cout << add(5, 3) << '\n'; // 应该得到8,结果得到2 + return 0; +} +``` + +这段代码有一点很好:错误非常明显,因为错误的答案会通过输出语句打印到屏幕上面。这位我们的调查提供了一个起点。 + +`找到根本原因`: 在第11行,我们可以看到我们正在传递参数的字面量(5和3),所以那里没有出错的余地。由于函数add的输入是正确的,但是输出不是。很明显,函数add一定是产生了错误的值。函数add中唯一的语句是返回语句,这一定是罪魁祸首。我们已经找到了问题,现在我们知道了我们的关注点在那里,注意到我们在做减法而不是加法。 + +`理解问题`:在这种情况下,很明显为什么会产生错误的值 -- 我们使用了错误的操作符 + +`确定修复方案`:我们只需要将operator - 改成 operator +。 + +`修复问题`:将operator- 改成 operator+,重新编译程序。 + +`重新测试`:在实施改变后,重写运行程序,我们得到了正确的结果8。对于这个简单的程序来说,这就是所有需要的测试。 + +这个例子很简单,但它说明了诊断任何程序时要经历的基本过程。 \ No newline at end of file diff --git "a/03DebugC++\347\250\213\345\272\217/3.3 \350\260\203\350\257\225\347\255\226\347\225\245.md" "b/03DebugC++\347\250\213\345\272\217/3.3 \350\260\203\350\257\225\347\255\226\347\225\245.md" new file mode 100644 index 0000000..93c01f2 --- /dev/null +++ "b/03DebugC++\347\250\213\345\272\217/3.3 \350\260\203\350\257\225\347\255\226\347\225\245.md" @@ -0,0 +1,85 @@ +# 调试策略 + +在调试程序的时候,大多数情况下,你的绝大多数时间会花在试图找到错误发生的地方。一旦找到了问题,剩下的步骤(修复问题和验证问题是否被修复)相比之下往往是微不足道的。 + +在本节中,我们将开始探讨如何查找错误。 + +# 通过代码检查发现问题 + +比如说,你已经注意到了一个问题,你想追踪这个问题的原因。在许多情况下(尤其是在小的程序当中),我们可以迅速地找到问题的近似点。 + +考虑以下程序片段: + +```c++ +int main() +{ + getNames(); // 告诉用户输入名字 + sortNames(); // 使用字母序排序它们 + printNames(); // 打印排序后的名字 + return 0; +} +``` + +如果你希望这个程序按照字母顺序打印名字,但是它却以相反的顺序打印,那么问题可能出现在sortNamess函数上。如果你能把问题缩小到一个特定的函数上,你可能只需要看一下代码就能发现问题。 + +然而,随着程序越来越复杂,通过代码检查发现问题也变得越来越复杂。 + +首先,要看的代码更多。在一个长达数千行的程序中,查看每一行代码都会花费很长的时间(更不要说这很无聊)。第二,代码本身往往十分复杂,可能出错的地方很多。第三,代码的行为可能不会给你很多线索,让你知道哪里出了问题。如果你写了一个推荐股票的程序,但是实际上它什么也没有输出,你可能不会有太多的线索,不知道该从哪里开始寻找问题。 + +最后,bug可能是由错误的假设引起的。几乎不可能直观的发现由错误假设引起的bug,因为你很可能在检查代码时做出同样的错误假设,而没有注意到这个错误。那么如果我们通过检查代码无法发现问题,我们该如何发现呢? + +# 通过运行程序发现问题 + +幸运的是,如果我们无法通过检查代码发现问题,我们可以采取另外一种途径:我们可以观察运行时的行为,并试图从中诊断出问题。这种方法可以概括为: + +1. 相出如何重现问题 +2. 运行程序并收集信息,以缩小问题所在的范围。 +3. 重复前面的步骤,直到找到问题为止 + +在本章的其他部分,我们将讨论这种方法。 + +# 重现问题 + +发现问题的第一步,也是最重要的一步,就是要能够重现问题,原因很简单:除非你能观察到问题的发生,否则极难发现问题。 + +回到我们的饮冰机的比喻 -- 假设有一天你的朋友告诉你,你的饮冰机坏了。你去看了一下,它正常工作。你会如何诊断呢?这将是非常困难的。然而,如果你能真正的看到饮冰机不正常工作,那么你就可以开始更有效的诊断它为什么不工作。 + +如果一个软件问题是非常明显的(比如,程序每次运行到同一个地方都会崩溃),那么重现问题可能是简单的。然而,有时候重现一个问题非常麻烦。问题可能只会发生在某些计算机上面,或者在特定的情况下(比如,当用户输入某些值的时候)。在这种情况下面,搞一个重现步骤是很有帮助的。`重现步骤`是一个清晰和精确的步骤列表,可以遵循这些步骤,以高度的可预测性导致问题再次发生。我们的目标是能够尽可能的使问题重现,这样我们就可以反复运行我们的程序,寻找线索来确定问题的原因。如果能够100%重现,那是最理想的,但是低于100%的重现也是可以的。一个只有在50%的时间内发生的问题只是意味着诊断问题需要花费两倍的时间,因为有一半的时间程序不会表现出问题,因此不会贡献任何御用的诊断信息。 + +# 专注于问题 + +一旦我们可以合理的重现问题,下一步就是找出问题在代码中的位置。根据问题的性质,这可能很容易,也可能很困难。为了举例说明,假设我们不知道问题到底在哪里。我们如何找到它呢? + +打个比方就能很好的解决这个问题。让我们来玩个游戏吧。我要请你们猜一个1到10之间的数字。对于你的每一次猜测,我会告诉你是太高还是太低,还是正确。这个游戏的例子可能是这样的。 + +```text +You: 5 +Me: Too low +You: 8 +Me: Too high +You: 6 +Me: Too low +You: 7 +Me: Correct +``` + +在上面的游戏中,你不需要每一个数字都猜就能找到我所想的数字。猜测的过程中,考虑从每次猜测中获得信息,你就只需要猜几次就能“锁定”正确的数字(如果你使用最佳策略,你总是能在4次或者更少的猜测中找到我在想的数字)。 + +我们可以使用类似的过程来调试程序。在最糟糕的情况下,我们可能不知道错误在哪里。但是,我们知道,问题一定是在程序开始到表现出第一个我们能观察到错误之间的代码的某一个地方。这至少排除了在第一个观察到错误之后执行的部分。但是这依旧覆盖了很多代码。为了诊断问题,我们将对问题进行一些有根据的猜测,目的是快速锁定问题的所在。 + +通常情况下,不管是什么原因导致我们注意到这个问题,都会给我们一个初步的猜测,这个猜测接近于实际问题的所在。比如,如果程序在应该写入数据的时候没有将数据写入到文件,那么问题可能就出在处理写入文件的代码中的某个地方。那么我们可以使用类似hi-lo的策略来尝试隔离问题的实际所在。 + +比如: + +- 如果在我们的程序中的某一点,我们可以证明问题还没有发生,这就类似于收到一个"太小"的hi-lo结果 -- 我们知道问题一定是在程序后面的某个地方。比如,如果我们的程序每次都在同一个地方崩溃,而我们可以证明程序在某一点上没有崩溃,那么崩溃代码一定是在后面 +- 如果在我们的程序中的某一点,我们可以观察到与问题相关的不正确的行为,那么这就类似于收到了一个“太大”的hi-lo结果,我们知道问题一定是在前面。比如,假设一个程序打印了某个变量x的值,你本以为它会打印值2,但它却打印了8。变量x的值一定是错的。如果,在程序执行过程中的某个点,我们可以看到变量x的值已经是8,那么我们就知道问题一定在这个点之前发生了。 + +hi-ho的类比并不完美 -- 我们有时候也会将一整块代码删除,而没有获得任何关于实际问题是在这一点之前还是之后的信息(TODO 有些不通顺) + +我们将在下一节中展示这三种情况的例子。 + +最终,有了足够的猜测和一些好的技巧,我们就可以找到导致问题的确切原因了!如果我们做了错误的假设,这将帮助我们发现,当你排除了其他因素之后,唯一剩下的一定是导致问题的原因。那就只需要了解原因了。 + +你想使用什么样的猜测策略取决于你自己 -- 最好的策略取决于它是什么类型的bug,所以你可能会想尝试许多不同的方法来缩小问题的范围。随着你在调试问题方面经验的积累,你的直接将引导你。 + +那么,我们该如何“猜测”呢?有很多方法可以做到这一点。我们将从下一节开始从一些基础的方法开始,然后我们将在这些方法的基础之上,在以后的章节中探讨其他方法。 \ No newline at end of file diff --git "a/03DebugC++\347\250\213\345\272\217/3.4 \345\237\272\347\241\200\350\260\203\350\257\225\347\255\226\347\225\245.md" "b/03DebugC++\347\250\213\345\272\217/3.4 \345\237\272\347\241\200\350\260\203\350\257\225\347\255\226\347\225\245.md" new file mode 100644 index 0000000..489ab23 --- /dev/null +++ "b/03DebugC++\347\250\213\345\272\217/3.4 \345\237\272\347\241\200\350\260\203\350\257\225\347\255\226\347\225\245.md" @@ -0,0 +1,544 @@ +# 基础调试策略 + + + +在上一节中,我们探讨了通过运行程序和使用猜测来发现问题的策略,以确定问题所在。在本节中,我们将探讨一些基本策略,以实际进行这些策略和收集信息,帮助发现问题。 + +# 调试策略1: 注释你的代码 + +让我们从一个简单的策略开始。如果你的程序表现出错误的行为,有一种方法可以减少你必须查阅的代码量,那就是把一些代码注释掉,看看问题是否仍然存在。如果问题依旧存在,说明注释掉的代码没有问题。 + +请看下面的代码: + +```c++ +int main() +{ + getNames(); // 让用户输入一系列名字 + doMaintenance(); // 执行一些随机操作 + sortNames(); // 使用字母序排序 + printNames(); // 打印已排序的名字列表 + return 0; +} +``` + +比如说,这个程序应该按照字母顺序打印用户输入的名字,但是它却按照相反的字母顺序打印。问题出在哪里?是getNames输入的名字不正确吗?sortNames是否执行了排序?printNames是否执行了打印? 可能是其中的一个出了问题。但是我们怀疑doMaintenance和问题无关,所以我们把它注释掉 + +```c++ +int main() +{ + getNames(); // 让用户输入一系列名字 + // doMaintenance(); // 执行一些随机操作 + sortNames(); // 使用字母序排序 + printNames(); // 打印已排序的名字列表 + return 0; +} +``` + +如果问题消失了,那么doMaintenance一定是造成问题的原因,我们应该把注意力放在那里。 + +然而,如果问题依然存在(这种可能性更大),那么我们就知道doMaintenance没有出错,我们可以将整个函数从搜索中排除。这并不能帮助我们了解实际问题是在调用doMaintenance之前还是之后,但是它减少了我们后续需要查看的代码量。 + +不要忘记你注释掉了哪些功能,记得以后取消。 + +# 调试策略2: 验证你的代码流程 + +另外一个在比较复杂的程序中常见的问题是,程序对一个函数的调用次数或多或者或少(或者根本没有)。 + +在这种情况下,将打印语句放在函数的头部。这样,当程序运行的时候,你就可以看到哪些函数被调用了。 + +### 提示 + +当为了调试目的而打印信息的时候,使用std::cerr而不是std::cout,其中一个原因是std::cout 可能会缓冲,这意味着你要求std::cout输出信息和它实际输出信息之间可能会有一个停顿。如果你使用std::cout输出,然后你的程序紧接着就崩溃了,那么std::cout可能还没有被输出,这可能会误导你,让你不知道问题出在哪里。另一个方面,std::cerr是无缓冲的,这意味着你发送给它任何东西都会立即输出。这有助于确保所有的调试输出尽快出现(代价是牺牲一些性能,我们通常在调试的时候并不关心性能)。 + +考虑一下见的简单的程序不能正确工作: + +```c++ +#include + +int getValue() +{ + return 4; +} + +int main() +{ + std::cout << getValue; + return 0; +} +``` + +虽然我们希望这个程序能够打印出4,但实际上在不同的机器上打印的值是不一样的。在笔者的机器上,它打印的是 + +```text +00101424 +``` + +让我们为这些函数添加一些调试语句 + +```c++ +#include + +int getValue() +{ + std::ceer << "getValue() called\n"; + return 4; +} + +int main() +{ + std::ceer << "main() called\n"; + std::cout << getValue; + return 0; +} +``` + +### 提示 + +当添加临时debug语句的时候,最好不要缩进。这样更容易找到它们,以方便后期删除。(译者注:我总是习惯性格式化代码,这个提示没什么用) + +现在,当程序止执行的时候,它们会输出它们的名字,表明它们被调用了 + +```text +main() called +00101424 +``` + +现在我们可以看到,函数getValue从未被调用,一定是调用函数的代码有问题。让我们仔细看这一行 + +```c++ +std::cout << getValue; +``` + +哦,看,我们忘记了调用函数的括号,应该是: + +```c++ +#include + +int getValue() +{ + std::ceer << "getValue() called\n"; + return 4; +} + +int main() +{ + std::ceer << "main() called\n"; + std::cout << getValue(); // 添加括号 + return 0; +} +``` + +现在将产生正确的输出 + +```text +main() called +getValue() called +4 +``` + +而且我们可以删除临时的调试语句 + +# 调试策略3: 打印值 + +对于某些类型的错误,程序可能会计算或者传递错误的值。 + +我们还可以输出变量(包括参数)或表达式的值,以确保其正确性。 + +考虑下面的程序,它应该是两个数字相加,但不能正常工作。 + +```c++ +#include + +int add(int x, int y) +{ + return x + y; +} + +void printResult(int z) +{ + std::cout << "The answer is: " << z << "\n"; +} + +int getUserInput() +{ + std::cout << "Enter a number: "; + int x{}; + std::cin >> x; + return x; +} + +int main() +{ + int x{ getUserInput() }; + int y{ getUserInput() }; + std::cout << x << " + " << y << '\n'; + int z{ add(x, 5) }; + printResult(z); + return 0; +} +``` + +下面是这些程序的一些输出 + +```text +Enter a number: 4 +Enter a number: 3 +4 + 3 +The answer is: 9 +``` + +这是不对的。你看到错误了吗?即使在这个很短的程序中,也很难发现。让我们添加一些代码来调试我们的值 + +```c++ +#include + +int add(int x, int y) +{ + return x + y; +} + +void printResult(int z) +{ + std::cout << "The answer is: " << z << '\n'; +} + +int getUserInput() +{ + std::cout << "Enter a number: "; + int x{}; + std::cin >> x; + return x; +} + +int main() +{ + int x{ getUserInput() }; + std::ceer << "main:x = " << x << '\n'; + int y{ getUserInput() }; + std::ceer << "main:y = " << y << '\n'; + + std::cout << x << " + " << y << '\n'; + + int z{ add(x, 5) }; + std::ceer << "main:z = " << z << '\n'; + printResult(z); + return 0; +} +``` + +这是上面的输出 + +```text +Enter a number: 4 +main::x = 4 +Enter a number: 3 +main::y = 3 +4 + 3 +main::z = 9 +The answer is: 9 +``` + +变量x和y都得到了正确的值,但是变量z却没有。问题一定是在这两点之间,这就使得add函数称为一个关键的疑点。 + +让我们来修改一下add函数 + +```c++ +#include + +int add(int x, int y) +{ + std::ceer << "add() called (x=" << x << ", y=" << y << ")\n"; + return x + y; +} + +void printResult() +{ + std::cout << "The answer is: " << z << '\n'; +} + +int getUserInput() +{ + std::cout << "Enter a number: "; + int x{}; + std::cin >> x; + return x; +} + +int main() +{ + int x{ getUserInput() }; + std::cerr << "main:x = " << x << '\n'; + int y{ getUserInput() }; + std::cerr << "main:y = " << y << '\n'; + + std::cout << x << " + " << y << '\n'; + int z{ add(x, 5) }; + std::cerr << "main:z = " << z << '\n'; + printResult(z); + return 0; +} +``` + +现在,我们得到以下输出 + +```text +Enter a number: 4 +main::x = 4 +Enter a number: 3 +main::y = 3 +add() called (x=4, y=5) +main::z = 9 +The answer is: 9 +``` + +变量y的值是3,但是不知道为什么我门add函数的参数y的值是5,我们一定是传错了参数。果然 + +```c++ +int z{ add(x, 5) }; +``` + +就是这样。我们传递了5而不是变量y的值作为参数。这很容易修复,然后我们就可以删除调试语句了。 + +# 再举一个例子 + +这个程序和上一个非常相似,但是同样不工作。 + +```c++ +#include + +int add(int x, int y) +{ + return x + y; +} + +void printResult(int z) +{ + std::cout << "The answer is: " << z << '\n'; +} + +int getUserInput() +{ + std::cout << "Enter a number: "; + int x{}; + std::cin >> x; + return --x; +} + +int main() +{ + int x{ getUserInput() }; + int y{ getUserInput() }; + + int z{ add(x, y) }; + printResult(z); + return 0; +} +``` + +如果我们运行这段代码,会看到以下内容 + +```text +Enter a number: 4 +Enter a number: 3 +The answer is: 5 +``` + +emmm, 有些不对劲,但是哪里不对劲呢? + +让我们对这段代码进行调试 + +```c++ +#include + +int add(int x, int y) +{ + std::cerr << "add() called(x=" << x << ", y=" << y << ")\n"; + return x + y; +} + +void printResult(int z) +{ + std::cerr << "printResult() called(z=" << z << ")\n"; + std::cout << "The answer is: " << z << '\n'; +} + +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::cerr << "main:x = " << x << '\n'; + int y{ getUserInput() }; + std::cerr << "main:y = " << y << '\n'; + + int z{ add(x, y) }; + std::cerr << "main:z = " << z << '\n'; + printResult(z); + return 0; +} +``` + +现在让我们使用同样的输出再次运行程序。 + +```text +main() called +getUserInput() called +Enter a number: 4 +main::x = 3 +getUserInput() called +Enter a number: 3 +main::y = 2 +add() called (x=3, y=2) +main::z = 5 +printResult() called (z=5) +The answer is: 5 +``` + +现在我们可以立即看到一些问题:用户输入的是4,但是main的x却得到了3。在用户输入的地方和将值分配给main的变量x的地方一定有哪里出了问题。让我们通过给getUserInput添加一些调试代码来确保程序从用户那里得到了正确的值。 + +```c++ +#include + +int add(int x, int y) +{ + std::cerr << "add() called(x=" << x << ", y=" << y << ")\n"; + return x + y; +} + +void printResult(int z) +{ + std::cerr << "printResult() called(z=" << z << ")\n"; + std::cout << "The answer is: " << z << '\n'; +} + +int getUserInput() +{ + std::cerr << "getUserInput() called\n"; + std::cout << "Enter a number"; + int x{}; + std::cin >> x; + std::cerr << "getUserInput:x = " << x << '\n'; // 添加该行debug + return --x; +} + +int main() +{ + std::cerr << "main() called\n"; + int x{ getUserInput() }; + std::cerr << "main:x = " << x << '\n'; + int y{ getUserInput() }; + std::cerr << "main:y =" << y << '\n'; + + int z{ add(x, y) }; + std::cerr << "main:z = " << z << '\n'; + printResult(z); + return 0; +} +``` + +输出 + +```text +main() called +getUserInput() called +Enter a number: 4 +getUserInput::x = 4 +main::x = 3 +getUserInput() called +Enter a number: 3 +getUserInput::x = 3 +main::y = 2 +add() called (x=3, y=2) +main::z = 5 +printResult() called (z=5) +The answer is: 5 +``` + +通过这一行的额外测试,我们可以看到用户的输入被正确的接收到getUserInput的变量x中,然而不知道为什么main的变量x却得到了错误的值。问题一定在这两点之间。唯一的罪魁祸首就是函数getUserInput的返回值。让我们再仔细看看这一行。 + +```c++ +return --x; +``` + +emm, 很奇怪,为什么x前面的是`--`?我们在目前的教程中还没有涉及到这个问题,所以如果你还不知道它的意思,也不需要担心。但是即使你不知道它的意思,通过你的调试工作,你也可以合理的确定这一行出现了问题。因此,很可能是这个`--`符号造成了问题。 + +既然我们真的想让getUserInput只返回x的值,那么让我们去掉`--`看看会发生什么。 + +```c++ +#include + +int add(int x, int y) +{ + std::cerr << "add() called(x = " << x << ",y=" << y << ")\n"; + return x + y; +} + +void printResult(int z) +{ + std::cerrr << "printResult() called(z=" << z << ")\n"; + std::cout << "The answer is: " << z << '\n'; +} + +int getUserInput() +{ + std::cerr << "getUserInput() called\n"; + std::cout << "Enter a number: "; + int x{}; + std::cin >> x; + std::cerr << "getUserInput::x = " << x << '\n'; + return x; // 去掉x之前的 -- +} + +int main() +{ + std::cerr << "main() called\n"; + int x{ getUserInput() }; + std::cerr << "main:x =" << x << '\n'; + int y{ getUserInput() }; + std::cerr << "main:y =" << y << '\n'; + + int z{ add(x, y) }; + std::cerr << "main:z =" << z << '\n'; + printResult(z); + return 0; +} +``` + +现在输出 + +```text +main() called +getUserInput() called +Enter a number: 4 +getUserInput::x = 4 +main::x = 4 +getUserInput() called +Enter a number: 3 +getUserInput::x = 3 +main::y = 3 +add() called (x=4, y=3) +main::z = 7 +printResult() called (z=7) +The answer is: 7 +``` + +现在程序正常工作了。即使不明白`--` 做什么。我们也能够确定导致问题的具体代码行,然后解决这个问题。 + +# 为什么使用打印语句调试不是很好 + +虽然为了诊断目的在程序中添加调试语句是一种很常见的基础技术,也是一种功能性技术(特别是当由于某些原因无法使用调试器的时候),但是由于一些原因,它并不是那么好用。 + +1. 调试语句让你的代码很混乱 +2. 调试语句让代码的输出变得很混乱 +3. 调试语句必须在调试完成之后删除,这使得它们无法被复用 +4. 调试语句需要你对你的代码进行修改,需要添加,也需要删除,这可能会带来新的错误。 + +我们可以做的更好,我们将在后面的课程中继续探讨。 \ No newline at end of file diff --git "a/03DebugC++\347\250\213\345\272\217/3.5 \346\233\264\345\244\232\350\260\203\350\257\225\347\255\226\347\225\245.md" "b/03DebugC++\347\250\213\345\272\217/3.5 \346\233\264\345\244\232\350\260\203\350\257\225\347\255\226\347\225\245.md" new file mode 100644 index 0000000..56513a6 --- /dev/null +++ "b/03DebugC++\347\250\213\345\272\217/3.5 \346\233\264\345\244\232\350\260\203\350\257\225\347\255\226\347\225\245.md" @@ -0,0 +1,142 @@ +# 更多调试策略 + + + +在上一节中(3.4 - 基础调试策略)。我么开始探索如何手动进行调试。在那一节中,我们对使用语句来打印调试文本提出了一些批评意见。 + +1. 调试语句让你的代码很混乱 +2. 调试语句让代码的输出变得很混乱 +3. 调试语句必须在调试完成之后删除,这使得它们无法被复用 +4. 调试语句需要你对你的代码进行修改,需要添加,也需要删除,这可能会带来新的错误。 + +我们可以缓解其中的一些问题。在本节中,我们将探索一些基础的技术手段来做到这一点。 + +# 调试代码的条件化 + +考虑以下包含一些调试语句的程序 + +```c++ +#include + +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; + return 0; +} +``` + +当你完成调试语句后,你需要把它们删除,或者注释掉。然后,如果你还想要它们,你就需要把它们加回来,或者取消注释。 + +在整个程序中更容易禁用和启用调试的一种方法是使用预处理器指令使你的调试语句成为有条件的。 + +```c++ +#include + +#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; + return 0; +} +``` + +现在,我们可以通过注释/取消注释#define ENABLE_DEBUG来启用调试。这让我们可以重复使用之前添加的调试语句,当我们调试完成之后,只需要禁用它们就可以了,而不需要将它们从代码中删除。如果这是一个多文件程序,`#define ENABLE_DEBUG`会被放到一个头文件中,包含在所有的代码文件中,这样我们就可以在一个位置注释/取消注释 `#define`,并让它传播到所有的代码文件。 + +这解决了必须删除调试语句的问题,但代价是代码更加混乱了。这种方法的另外一个缺点是,如果你写错了字(比如打错了DEBUG),或者忘记将头文件包含到代码文件中,该文件的部分或者全部调试可能无法启用。所以,虽然比上一个版本要好,但仍然有改进空间。 + +# 使用logger + +通过预处理器进行条件化的另一种方法是将调试信息发送到日志文件中。日志文件是一个记录软件中发生事件的文件(通常存储在磁盘上)。将信息写入日志文件的过程称为日志记录。大多数应用程序和操作系统都会编写日志文件,可以用来帮助诊断发生的问题。 + +日志文件有几个优点。因为写入日志文件的信息与程序的输出是分开的,所以你可以避免正常输出和调试输出混在一起造成混乱。日志文件也可以很容易的发送给其他人进行诊断 -- 所以如果有人使用你的软件出现了问题,你可以要求他们把日志文件发给你,这可能会帮助你找到问题的线索。 + +虽然你可以编写自己的代码来创建日志文件并写入日志,但你最好使用现有的许多第三方日志工具之一。使用哪一个由你决定。 + +为了说明问题,我们将展示使用[plog](https://github.com/SergiusTheBest/plog) 输出到日志的情况。 plog是以一组头文件的形式实现的,所以它很容易包含在你需要的任何地方,而且它很轻巧,易于使用。 + +```c++ +#include +#include // 步骤1: include头文件 + +int getUserInput() +{ + LOGD << "getUserInput() called"; // LOGD由plog库的定义 + + std::cout << "Enter a number: "; + int x{}; + std::cin >> x; + return x; +} + +int main() +{ + plog::init(plog::debug, "Logfile.txt"); // 步骤2: 初始化库 + LOGD << "main() called"; // 步骤3: 输出到日志中,就像写入控制台一样 + + int x{ getUserInput() }; + std::cout << "You entered: " << x; + return 0; +} +``` + +这是上述日志的输出(在Logfile.txt中) + +```text +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 +``` + +如何include、初始化、使用日志库将根据你使用的日志库有所不同。 + +请注意:使用此方法不需要条件编译指令,因为大多数记录器都有一个减少/消除向日志写入的方法。这使得代码更容易阅读,因为条件编译会增加很多乱七八糟的内容。在使用plog的时候,可以通过将init语句修改为以下内容来暂时禁止日志记录。 + +```c++ +plog::init(plog::none, "Logfile.txt") // plog::none消除了大多数消息的写入,本质上是关闭日志 +``` + +我们在以后的课程中不会再使用plog,所以你可以先不用关心。 + +> ### 旁白 +> +> 如果你想自己编译上面的例子,或者在你自己的项目中使用plog,你可以按照这些说明来安装它。 +> +> 首先,获取最新的plog版本 +> +> - 访问[plog](https://github.com/SergiusTheBest/plog) 主页 +> - 点击release选项 +> - 下载最新版本的源代码 +> +> 接下来,将整个档案解压到硬盘的任何地方 +> +> 最后,对于每个项目,将`\plog-\include\` 目录添加为IDE的include目录。Visual Studio请[查看](https://www.learncpp.com/cpp-tutorial/a2-using-libraries-with-visual-studio-2005-express/) . Code::Blocks请[查看](https://www.learncpp.com/cpp-tutorial/a3-using-libraries-with-codeblocks/) +> +> (译注),吐槽,C++连个统一的包管理都没有,即使CMake已经如此流行,但是在这个教程里面还是不被涵盖,最终的包管理居然是此种手动下载和设置include的方案。 \ No newline at end of file diff --git "a/03DebugC++\347\250\213\345\272\217/3.6 \344\275\277\347\224\250\351\233\206\346\210\220\350\260\203\350\257\225\345\231\250,\345\215\225\346\255\245\350\260\203\350\257\225.md" "b/03DebugC++\347\250\213\345\272\217/3.6 \344\275\277\347\224\250\351\233\206\346\210\220\350\260\203\350\257\225\345\231\250,\345\215\225\346\255\245\350\260\203\350\257\225.md" new file mode 100644 index 0000000..32f4ca0 --- /dev/null +++ "b/03DebugC++\347\250\213\345\272\217/3.6 \344\275\277\347\224\250\351\233\206\346\210\220\350\260\203\350\257\225\345\231\250,\345\215\225\346\255\245\350\260\203\350\257\225.md" @@ -0,0 +1,77 @@ +# 使用集成调试器 单步调试 + + + +当你运行你的程序时,执行从main函数的顶部开始,然后顺序执行语句,直到结束。在你的程序运行的时候,程序都在跟踪很多事情:你正在使用的变量的值,哪些函数被调用了(这样当这些函数返回的时候,程序就会知道回到哪里去),以及程序内的当前执行点(这样它就知道下一步要执行哪条语句)。所有这些被跟踪的消息都被称为你的程序状态(简称状态)。 + +在前面的课程中,我们探讨了改变代码以帮助调试的各种方法,包括打印诊断消息或者使用记录器。这些都是在程序运行时检查其状态的简单方法。虽然如果使用得当,这些方法是有效的,但是它们仍然有缺点:它们需要改变你的代码,这需要时间,而且可能会引入新的错误,而且它们使你的代码混乱,让现有的代码更难理解。 + +在我们到目前为止所展示的技术背后,有一个未声明的假设:一旦我们运行代码,它就会运行完成(只暂停接受输入),我们没有机会在任何时候干预和检查程序的结果。 + +然而,如果我们能够移除这个假设呢?幸运的是,我们可以。大多数现代IDE都带有一个叫做调试器的集成工具,它正是为了完成这个任务而设计的。 + +# 调试器 + +`调试器`是一种计算机程序,它允许程序员控制程序的执行方式,并在程序运行时检查程序状态。比如,程序员可以使用调试器逐行执行程序,沿途检查变量的值。通过比较变量的实际值和预期值,或者观察代码的执行路径,调试器可以极大地帮助跟踪语义(逻辑)错误。 + +调试器背后的力量有两方面:一是能够精确控制程序的执行,二是能够查看(如果需要,还可以修改)程序的状态。 + +早期的调试器,比如[gdb](http://en.wikipedia.org/wiki/Gdb) 。是独立的程序,有命令行界面,程序员必须输入神秘的命令才能让它们工作。后来的调试器(比如Borland的turbo调试器的早期版本)仍然是独立的,但是有自己的“图形化”前端,使它们的工作更容易。现在大多数现代的IDE都有一个集成的调试器 --- 也就是说,调试器使用与编辑器相同的节目,所以你可以使用与你编写代码相同的环境进行调试(而不是必须切换程序)。 + +几乎所有的现代调试器都包含了相同的标准基本功能集 -- 然而,在访问这些功能的菜单如何安排上并不一致,键盘快捷键的一致性就更低了。虽然我们的例子会使用Visual Studio的截图(我们也会介绍如何在Code::Blocks中做这些事情),但是无论你使用的是哪种开发环境,甚至是我们没有介绍的环境,你都应该不难找出如何访问我们讨论的每个功能。 + +本章的剩余部分将用来学习如何使用调试器。 + +### 提示 + +不要忽视学习使用调试器。随着你的程序变得越来越复杂,与你节省的查找和修复问题的时间相比,你花在学习有效使用集成调试器上的时间就显得微不足道了。 + +### 警告 + +在继续学习本课之前(以及后续和使用调试器相关的课程),请确保你的项目使用调试器配置进行编译(参考0.9节 -- 配置你的编译器--构建配置 章节)。如果你使用发布配置来编译你的项目,调试器的功能可能无法正常工作。 + +### 对于Code::Blocks用户 + +如果你使用的是Code::Blocks,你的调试器可能会或者不会被正确设置。让我们来检查一下。 + +首先,进入 设置菜单 -->> 调试器 -->> GDB/CDB debugger, 选择Default。应该打开以下对话框,看起来像这样 + +![img](https://blogimg.ficapy.com/learncpp/CB-DebuggingSetup-min.png) + +如果你看到"可执行文件路径"的地方有一个大红条,那么你需要找到你的调试器。要做到这一点,点击“可执行路径”字段右边的按钮,在你的系统中找到gdb32.exe文件。存在于以下目录"*C:\Program Files (x86)\CodeBlocks\MinGW\bin\gdb32.exe*."。然后点击确定。 + +### 对于Code::Blocks用户 + +有报告称,Code::Blocks集成调试器(GDB)在识别一些包含空格或者非英文字符的路径时可能出现问题。如果你在学习这些课程的时候遇到问题,这可能是一个原因。 + +# 单步调试 + +我们要开始对调试器的探索,首先要研究一些调试工具,让我们能够控制程序的执行方式。 + +`Stepping`是一组相关调试器功能的名称,它让我们逐条语句执行我们的代码。 + +还有一些相关的单步命令,我们将依次介绍 + +# Step into + +`Step into`命令执行程序正常执行路径中的下一条语句,然后暂停程序的执行,这样我们就可以使用调试器检查程序的状态。如果正在执行的语句中包含一个函数调用,step会让程序跳转到被调用函数的顶端,在那里暂停执行。 + +让我们来看看一个非常简单的程序: + +```c++ +#include + +void printValue(int value) +{ + std::cout << value << '\n'; +} + +int main() +{ + printValue(5); + return 0; +} +``` + +让我们使用step into命令来调试这个程序。 +