大家好欢迎光临
在这节课中我想谈谈调试｡
这是一个非常重要的概念, 你需要特别理解, 你需要了解一些工具和一些方法,
你可以用来发现和修复错误｡
因此, 调试是在程序中查找和修复错误的过程｡
通常是逻辑错误｡
我将告诉你们逻辑错误和语法错误的区别, 但它也可以包括编译器语法错误｡
这是语法错误的整个过程｡
调试就是理解编译器告诉你的东西｡
您总是希望专注于修复检测到的第一个问题｡
我想通过一些常见的编译器错误和警告, 你可能会得到,
但你总是想知道的事情之一是, 编译器可能会给你一些错误｡
您总是希望关注第一个错误, 因为第一个错误可能会导致其他错误｡
因此, 如果你在一个程序中看到五个错误, 编译器告诉你, 你可能只有一个错误, 而第一个错误可能会导致其他错误,
这可能是非常误导的｡
因此, 重要的是要始终查看第一个顶级错误并首先修复该错误｡
在这个非常简单的例子中, 我们在代码中有多个错误｡
我们没有｡
我们没有左括号和花括号｡
我们没有分号｡
我们返回一个字符串｡
所以当我们保存它, 然后编译它的时候, 你会看到一些红色的错误,
当我们编译它的时候, 它会告诉我们有三个错误｡
您永远不希望只选择第7行上的最后一个错误, 即使在本例中它是一个实际的错误｡
你总是希望只修复第一个错误｡
所以你看到这里列出的第一个错误, 你修复了第一个错误, 然后重新编译, 再次运行｡
但是这样做的原因仅仅是为了修复顶级错误, 因为该错误可能会导致其他问题｡
因此, 始终专注于修复第一个错误, 调试以及查找和修复错误的过程可能非常复杂,
从查找简单的错误到收集大量数据进行分析｡
看情况
有时除了日志记录之外, 您没有提供任何信息, 在这种情况下, 您必须进行分析｡
有时候你会遇到一些不能重复的bug｡
你不能复制它, 因为它发生在如此罕见的场合｡
这通常是当你必须分析日志或一些一些变化｡
程序员的调试能力是一项基本技能｡
这确实是一种解决问题的技巧, 可以为您节省大量的时间和金钱｡
所以这是一个需要学习的重要技能｡
这是一项需要培养的重要技能｡
这需要练习
你不会就这么走了不会就这么走了｡
您可以应用某些技术和方法来提高调试能力｡
我认识很多非常擅长写软件代码的开发人员｡
他们对语言理解得很好, 但他们的调试技能很差｡
他们可能会得到一个错误, 他们解决这个错误的第一个倾向是真的有点偏离轨道｡
再一次, 这需要练习｡
你会越来越擅长调试和理解如何发现和修复问题, 但这也是你必须学习的｡
维护阶段是软件生命周期中最昂贵的阶段｡
维护阶段是在您发布产品之后, 现在您必须维护它｡
维护它意味着修复任何新的bug并添加新功能｡
因此, 例如, 当Windows发布新的操作系统（如Windows 10）时, 他们还没有完成该软件的生产｡
它可能要在野外待上十年｡
当任何新的错误出现或者他们发现新的错误时, 微软仍然有责任修复它｡
这就是为什么他们推动自动更新, 他们这样做的事情｡
然后, 他们也会注意到, 随着市场的变化和技术的变化, 操作系统将推出新的功能｡
所以维护是非常重要的｡
因此, 找到并修复bug非常重要｡
但要明白, bug是不可避免的｡
所有的软件都有bug｡
没有人能写出完美的代码, 甚至不一定要写出完美的代码｡
只是时间会改变｡
这也可能导致bug｡
你必须明白, 仅仅因为软件有bug并不意味着它的代码写得很差｡
这是不可避免的
所以, 当你发现并修复bug时, 发现bug是可以接受的, 因为你真的觉得自己在完成一些事情｡
常见问题｡
我们将讨论更多与视觉相关的具体问题｡
但是在调试时常见的问题是逻辑错误｡
逻辑错误与代码实际编译的语法错误类型相反｡
它工作, 但它没有做它打算做的事情｡
所以有某种逻辑或预期的功能是不正确的｡
也许你有一个IF语句, 你在错误的条件下跳进了那个if语句｡
也许你有一个循环, 循环没有执行正确的次数, 或者你没有正确排序｡
功能被破坏了, 但编译器说, 嘿, 一切看起来都很好｡
这是一个逻辑错误的例子｡
逻辑错误是指语法正确, 但由于某种逻辑问题而导致预期结果不正确｡
在这段代码中, 你可以看到for循环末尾有一个分号, 这在语法上是正确的｡
但是你期望这个for循环运行五次｡
但事实并非如此
它只运行一次｡
因此, 如果我们说构建并构建这个项目, 我们确实会看到一个警告, 这很好, 您应该始终修复所有警告｡
但它确实编译成功了｡
所以当我们运行这个项目时, 你会注意到它实际上只运行一次, 并打印出一次Hello
World｡
这是一个逻辑错误的例子｡
语法错误与编译器直接相关｡
这些都是不符合语言规则的东西｡
在语句的末尾没有分号｡
你没有结局, 对吧？
左括号, 括号｡
你少了一个支架｡
你没有使用正确的关键字, 诸如此类的事情｡
语法错误通常是由编译器引起的逻辑错误是由程序员在测试阶段引起的｡
内存损坏也是一个常见的问题, 尤其是在C中｡
如果在正确分配内存后没有释放内存, 可能会出现悬空引用等问题,
也可能会出现与未使用内存相关的内存问题｡
你可能会有内存泄漏｡
下面是一个内存错误的示例｡
在这段代码中, 我们实际上有一个内存泄漏, 所以我们还没有讨论指针｡
我们还没有讨论动态内存分配｡
但是在C中, 你可以在程序运行时分配内存｡
可以使用malloc函数｡
所以我们在这里标记, 我们分配内存, 但我们永远不会释放它｡
所以我们可以构建这个代码, 我们可以运行它, 一切都很好, 但我们实际上并没有释放这个错误｡
所以我们要纠正这个内存错误, 我们必须说free, 然后我们必须说free和X来释放那个指针｡
但无论何时分配内存, 都不会释放它｡
这是内存泄漏｡
另一个例子是, 如果我们不分配内存, 如果我们创建一个指针, 然后我们试图分配它,
我们会有内存损坏, 因为内存没有任何分配, 我们试图分配一个值给它｡
这只是一些关于内存､ 损坏､ 性能和可伸缩性的小例子｡
这可能是一个问题, 你的功能是预期的, 你的代码语法是正确的,
但它不是很有效, 或者你没有硬件支持它｡
因此, 如果你运行的是一个服务器, 而你没有足够的硬件来支持所有的请求, 它的运行速度会非常非常慢｡
或者, 如果你编写的程序可以处理10个用户, 但是当它有很多用户时, 可能速度没有那么快,
因为数据库陷入困境或其他任何情况｡
代码是可以工作的, 但是它的设计并不适合处理大量的用户或其他任何情况｡
缺乏凝聚力｡
这可能是代码中的一个问题, 它降低了与代码执行太多或函数执行太多相关的质量｡
它不是一个单一的焦点, 甚至一个应用程序有太多的功能, 有功能,
从来没有使用, 所以这可能需要重新设计｡
这是一个程序的例子, 它没有模块化, 没有内聚性｡
你有一个很大的函数, 它做了太多的事情, 几百行代码, 几千行代码｡
你想把一个函数分解成辅助函数, 独立的模块｡
这使得你的程序更有凝聚力, 更容易维护｡
因此, 与其像一个大型的主方法那样进行初始化读取输入, 查找一些,
查找差异, 查找乘积, 查找商, 您可以创建所有这些代码, 并将其放在助手模块中,
并将代码分离, 因此分离可以帮助更容易地找到和修复bug｡
内聚性在C中总是很重要的, 通常通过提供帮助方法来解决高内聚性｡
紧密耦合｡
这也与代码质量有关｡
这就是你有很多依赖的地方｡
耦合不同于内聚｡
凝聚力是你唯一的焦点｡
耦合是指函数之间或对象之间的依赖关系｡
我们讨论了耦合, 讨论了全局变量｡
如果一个全局变量在许多不同的函数中使用, 它可能会导致这些函数之间的依赖关系｡
所以当你写代码的时候, 你要努力的是, 你要写高质量的代码,
你要有非常高的内聚和非常低的耦合｡
调试过程｡
在找到并修复任何错误之前, 您必须做的第一件事就是了解问题｡
这与不要只是一头扎进去, 开始看代码有关, 坐下来与测试人员一起,
理解需求, 理解正在发生的事情｡
因为如果你不明白发生了什么, 你甚至无法理解｡
你甚至不应该试图解决它｡
一旦你理解了这个bug, 试着重现它, 因为重现bug可以帮助你收集更多的信息｡
并不总是能够重现错误, 因为它们可能在非常罕见的情况下发生｡
例如, 具有竞争条件甚至内存损坏的错误可能只发生在非常特定的用例中,
并且它们可能是间歇性的问题｡
是这样吗？
你不能复制它｡
它将变得越来越难调试｡
如果你能够复制它, 你通常可以收集足够的信息来修复它｡
大多数问题都是可以解决的｡
此外, 如果您是多线程环境､ 并行进程或线程问题, 则可能会遇到无法重现问题的问题｡
这些也很难调试, 但是一旦你弄清楚了问题到底是什么,
就一定要试着重现它, 一旦你能够重现, 如果你不能, 那么你就要试着应用某种分治的方法｡
你想尝试隔离bug, 简化问题, 因为能够减少会给你信息, 但它可能不一定告诉你bug在代码的哪一部分｡
因此, 为了简化问题, 您可以删除原始测试用例的一部分, 这样您就不会测试太多｡
你可以通过说, 哦, 它只发生在这个特定的情况下, 把问题隔离得更小｡
你也可以注释掉代码｡
我们讨论了只有存根而没有实现的函数｡
有时在测试中使用它, 以便它可以限制问题集｡
此外, 如果注释掉代码, 实际上可能发生bug的代码会更少｡
所以当你注释掉代码或回退更改时, bug可能会消失, 如果bug消失了, 这就告诉你,
你注释掉的代码可能是导致它的原因｡
你实际上想做的是把一个大程序变成很多小程序, 无论是通过测试还是注释｡
另一种常见的技术是对代码进行单元测试｡
如果一个单元测试失败了, 它可以告诉你代码中的逻辑或问题可能在哪里｡
一旦你隔离了bug, 你和它将帮助你在代码中找到问题的根源, 即导致问题的特定代码｡
有时这将使用调试工具, 如调试器｡
我们将详细讨论调试器, 但通常取决于您使用的IDE集成开发,
IDE附带有调试器｡
还有一些调试器可以独立运行｡
我们将在C编程中使用的一个重要的调试器叫做GDB｡
它是GNU调试器, 所以它是一个很好的工具, 实际上是一个图形化的工具,
你可以停止程序的执行并添加断点｡
然后当你停止执行时, 你可以在代码中逐行查看变量的值｡
因此, 您可以在代码运行时确切地看到代码中发生了什么｡
这是个无价的工具
只是它太有用了｡
我们将运行调试器｡
我将向您展示如何添加断点｡
我将向您展示如何设置监视, 以便您可以在变量更改时查看它们, 然后逐步执行代码｡
在你确定了问题的根源之后, 你就必须解决它｡
这需要大量的经验和实践｡
仅仅因为你找到了问题的根源, 你仍然可能无法仅仅根据你的经验来解决它｡
这有时包括重新设计或重构其他代码, 特别是在可伸缩性问题或性能问题上,
您可能必须重新设计以使软件更具可伸缩性｡
因此, 仅仅找到问题并修复它并不总是那么容易｡
问题有时候, 尤其是更难的问题, 比如线程或并行处理,
这些问题更难解决｡
但是随着你写的代码越来越多, 这将变得越来越容易｡
最后, 你必须疯狂地测试｡
总是测试测试许多不同的边界｡
好的情况下, 坏的情况下, 在案件之间, 以验证您确实已修复的问题｡
一些可用的技术和工具｡
一个常用的､ 非常常用的工具是使用print语句｡
你可以使用print语句来跟踪你的代码｡
所以在C语言中, 你可以使用print F语句, 你可以把它放在代码中的多行中来表示你所处的位置｡
这也很有用, 当你的程序崩溃时非常有用｡
它可以帮助确定它在哪里坠毁｡
在添加这些print语句跟踪代码时, 可以在print语句中添加输出以查看变量的值,
因为变量的值可能会影响状态并可能导致逻辑问题｡
所以仅仅添加print语句就非常非常有用｡
它可以显示执行流程, 并可以帮助隔离错误｡
调试器, 正如我在GDB中提到的, 可以监视程序的执行｡
你可以停止它, 你可以逐行运行, 重新启动它, 设置断点, 监视变量和内存｡
非常非常有帮助, 当试图诊断问题所在｡
日志文件｡
日志文件也非常有用, 特别是对于无法重新创建的bug｡
您可以在稍后的日期分析它们, 以确切地了解错误发生时发生了什么｡
因此, 这意味着您必须在代码本身中提供良好的日志记录语句,
以便使其有用｡
因此, 您可以创建自己的日志文件, 然后在程序运行时写入该日志, 以便在出现错误时将这些日志发送给开发人员｡
我们在施乐公司经常使用这种方法, 因为当我们遇到问题时, 我们不能总是重现它们, 或者它们可能会在现场遇到｡
我们需要能够访问这些日志, 以查看该错误发生时发生了什么｡
监控软件｡
有一些软件可以监控和分析内存使用情况､ 网络流量威胁和对象信息｡
这可以告诉你是否分配了太多的内存而没有删除它｡
如果你有一个内存使用网络流量的工具, 可以告诉你是否发送了太多的数据包,
或者你的软件是否是可扩展的, 如果你有一个网络流量的增加, 你看到你的网络流量,
你的响应时间正在下降｡
您还可以监视线程和对象信息以进行并行处理, 从而了解某些线程中正在进行的操作以及另一个线程中正在进行的操作｡
这将帮助您诊断任何线程类型的问题｡
其他一些常见的调试工具, 您可以使用它们来查找和修复错误｡
您可以使用异常处理｡
许多编程语言都有异常处理｡
不幸的是, C没有一个异常处理程序, 它实际上可以防止崩溃, 并在运行时做很多事情｡
它不适用于看, 但它仍然是一件好事, 以了解｡
尝试拦截, 诸如此类的东西｡
静态分析仪｡
有许多静态分析器可用于C静态分析器｡
分析源代码而不分析实际的可执行文件｡
运行程序不需要运行｡
它逐行查看源代码｡
在解析源代码时, 它会查找任何已知的问题｡
所以它可以检查语义｡
它可以确保｡
就是这样
你没有任何未初始化的变量, 如果你分配新的内存, 你会删除它｡
如果您有任何实际上没有执行的死代码, 您可以删除该代码｡
它还分析竞争条件和死锁等问题｡
我工作的地方有几台静态分析仪｡
我们有一个叫做Coverity的工具, 它很受欢迎, 还有一个我们也使用的代码声纳工具, 我们将在我们的整个基线上运行它,
我们的整个源集｡
每当我们引入新文件和新代码时, 该工具将在该代码上运行, 并能够识别代码中的即时问题,
例如未初始化的变量, 内存泄漏等｡
它不分析语法｡
这就是编译器的作用｡
还有许多测试套件可以运行一组全面的系统端到端测试, 您可以在软件上运行这些测试｡
测试总是很重要的, 但是如果你能以某种方式自动化它, 例如每次你做一个新的构建时都运行某些测试,
这可能非常有帮助, 这实际上可以防止错误, 但它也有助于诊断错误｡
在程序崩溃后进行调试｡
我们将讨论调用堆栈｡
这是一个非常有用的机制, 特别是在像C这样的语言中, 当你崩溃时,
你得到的唯一信息就是分段错误｡
当你遇到这种崩溃时, 很多时候会发生什么, 取决于它是如何崩溃的, 一个核心文件将被生成,
你可以分析核心文件, 查看调用堆栈, 看看程序到底死在哪里｡
它会告诉你程序死亡的代码行｡
我们将学习调用堆栈｡
至于分析核心文件, 这是一种更先进的技术｡
但是在C中, 可以生成一个核心文件, 你可以分析它｡
这是一个更高级的技术, 但这是另一种方法, 你可以做你的软件调试｡
如何防止错误？
我们知道调试就是查找和修复错误, 但是如果你能防止错误, 那就更好了｡
防止错误的方法之一是编写高质量的代码｡
这不是很多软件工程师关注的焦点｡
很多软件工程师编写代码, 只要代码能工作, 这就是他们所关心的,
或者只要输出是正确的, 那么他们就认为自己完成了｡
但是有这样一件事, 质量差的代码, 你不想写｡
我已经在课堂上演示了一些, 但是像使用有意义的变量名这样的事情非常重要｡
或者如果你写的代码效率很低, 你使用的循环可能比你需要的要多, 或者使用if语句,
或者你不需要这样做｡
所有这些类型的东西都构成了低质量的代码｡
如果你有一个函数, 做100个其他的事情, 它不是内聚的, 如果你有5000行的函数,
如果你的代码不是以模块化的方式组织的, 或者如果你有很多函数之间的依赖关系,
再次, 所有这些都是质量差的代码, 你想努力不这样做｡
您希望在组织程序的地方使用技术｡
它易于维护､ 高度内聚､ 低耦合, 并且使用有意义的变量名｡
非常非常重要
同样, 这也非常重要, 因为维护阶段非常昂贵｡
如果你写的代码质量很差, 那么维护起来就会困难得多｡
隔离错误和添加新功能都很难｡
所以在编写代码时要使用高质量的技术｡
单元测试｡
单元测试有助于极大地防止错误｡
单元测试通常在编译单元测试时自动执行, 这有助于避免回归｡
这是最重要的因素之一｡
回归是当你写新代码时, 你在以前工作的东西上引入了一个bug｡
你打破了旧规则｡
你不会想那么做的
如果某个软件正在工作, 然后你做了一些新的事情破坏了它, 你希望立即得到通知,
单元测试可以帮助你做到这一点, 因为单元测试将在旧代码上运行, 它会说, 嘿, 这是工作,
现在它失败了｡
它还可以在新代码交付之前找到其中的错误, 因为它们在您构建后运行, 它会立即告诉您有什么问题｡
有一个测试驱动开发的概念正变得非常非常流行｡
这是您首先编写测试, 然后开发代码的地方｡
这里的目标是当你写第一个测试时, 你的测试失败了, 然后你写代码让它通过｡
TDD有很多优点, 它是一种逆向思维方式｡
通常人们在写代码时, 他们先写代码, 然后再写测试｡
但是使用TDD, 它实际上给了你一种不同的思维方式, 这样你就可以在很小的块中工作,
你倾向于编写非常有凝聚力的代码, 你没有代码, 没有很多依赖性, 你猜怎么着？
如果你能够测试代码, 这意味着代码没有那么多的依赖关系｡
如果你的单元测试非常非常大, 并且你要模拟所有这些不同的东西,
这通常是一个很大的指标, 表明你的代码非常非常依赖于其他东西｡
因此, 通过使用TDD, 它迫使您编写非常松散耦合的代码, 这些代码没有依赖关系,
并且它迫使您的函数非常具有内聚性｡
TDD有很多优点｡
您还希望提供良好的文档, 并希望进行适当的规划｡
不要只是埋头编写代码然后开始实现｡
把你的设计写在纸上｡
使用伪代码｡
好吧｡
规划您的代码｡
如果你计划你的代码, 你理解需求, 你有一个很好的深思熟虑的设计, 你可以防止很多很多错误｡
我在整个课程中都提到过, 但是有一个非常非常重要的分而治之的方法,
一小步一小步地工作, 在每一步之后不断地测试, 不要写了1000行代码, 然后说,
好了, 编译和测试｡
这不是一个好办法｡
避免一次更改太多｡
在进行更改时, 增量地应用它们, 添加一个更改, 然后在开始下一步之前进行彻底的测试｡
这可以帮助减少各种各样的bug, 也可以限制你的问题集｡
所以如果你有一个bug, 你马上就知道它在代码中的位置, 因为你刚刚编译过,
你刚刚添加了那一行或几行｡
同样, 我不能强调调试编写高质量代码, 防止错误的重要性｡
这些解决问题的技巧将为你节省大量的时间, 使你成为一个更好的程序员｡
所以在接下来的几节课中, 我想谈谈我们在c/c编程语言中看到的一些常见错误｡
另外, 我想谈一谈调用堆栈｡
我想通过调试器向您介绍菜单选项以及如何停止和运行监视等｡
我还想帮助您识别一些常见的编译器错误, 您可以看到如何解释编译器, 编译器警告和错误｡
如果编译器这样说, 这意味着什么？
如果编译器给你这个警告, 这意味着什么？
谢谢大家｡