你最喜欢在C++中使用哪些调试技巧?

6

有printf,asserts,编辑和继续,日志框架等等,你最喜欢哪个?


8
一定是咖啡。 - Dirk Eddelbuettel
6
作为一道没有确定答案的问题,这应该是一个社区维基页面。 - SPWorley
我认为这是一个重复的问题,因为你的“c ++”变体似乎并没有增加太多内容:http://stackoverflow.com/questions/91527/debugging-techniques - gnovice
请教我! - ZeroCool
20个回答

20

ASSERT(断言)是必须的。

我的个人代码库有30万行(不包括注释),高度分离和重用,其中大约15%(猜测)是模板,5万行代码是测试代码。

如果一个习惯用语被重复使用,则将其制作为函数/方法。个人认为,容易剪切和粘贴是魔鬼的发明,旨在膨胀代码并传播缺陷。

该库的大约4%是ASSERTS和调试代码(非常少的printf和几乎所有输出都是排队输出到低优先级任务的cout流中,因为屏幕IO太昂贵,因此会改变时间)。也许50%的asserts是为了保证类不变量和方法执行后条件的正确性。

当我重新访问可能匆忙或只是在接口/对象耦合设计中犯错的代码时,我会无情地重构,比如说方法的主体对象真正属于对象对象,并且该方法属于最初的对象对象之一(参数对象)。宽松使用assert似乎能保护我免受愚蠢的错误,如果我进行了实质性的重构。这种情况并不经常发生,但是有时候确实会发生。

我有一个DEBUGGING宏,它的作用类似于ASSERT,因此我可以将代码包含在其中:

DEBUGGING(... code....);

在非调试版本中不会编译它。

我不使用供应商提供的assert。我的assert不会中止和核心转储,它们只是弹出一个消息框并调用调试器。如果这是新代码并且方法是const方法,则能够返回到该方法,然后使用相同的参数集重新执行它(该方法)非常有用。有时即使某些数据发生了更改也与问题无关,人们也可以重新调用以获得知识收益。

我绝对讨厌命令行调试器。就像回到25年前一样-最好使用电传打字机和2400波特率线路。我需要和想要一个完整的IDE,可以右键单击数据结构并打开、关闭、追踪指针执行方法等等等等。

我会逐行检查我的新代码,并检查每个(我的)变量是否有预期的行为。在这里使用可以突出更改的IDE是非常宝贵的。要通过GDB做到这一点,必须成为一位具有卡纳克大师的记忆力的音乐家。

对于新开发,当遇到异常情况时,我还尝试捕获流数据/消息数据。这对UDP服务器特别有用,经常可以提前开始可重现性。

我也喜欢拥有可以“包围应用程序并驱动它并从中消耗并进行验证”的模拟器。(联合/耦合源/汇模拟器)我的几乎所有代码都是无头或至少与人类交互与功​​能无关,因此“包围”应用程序通常是可能的。我认为拥有理解测试数据创建非常重要且测试数据收集构建为能够演变成全面回归/冒烟测试套件的良好支持和管理非常重要。

我曾经喜欢将操作系统调度量子设置得非常低。对于多线程应用程序,这种短的量子更容易带出线程错误。我特别喜欢使用许多线程来驱动线程安全的对象方法-十几个甚至上百个线程。一般来说,如果应用程序是人驱动的,则无法在适当位置测试线程安全对象。因此,确实需要自定义测试驱动程序,这些驱动程序处于更低(组件定向)级别。并且在这些测试中,asserts可以让您知道是否出现了问题。显然,这并不能证明代码是正确的,但确实会给出一些信心。

这些偏好也可能反映了我所担任的更具类库/可重复使用性的角色和观点。当您编写库代码时,通常没有“生产”问题,因为根据定义,库被广泛使用和经过充分测试。日志记录和那种历史记录似乎更加面向应用程序而不是面向库。


3
哇,好的,我喜欢那个答案。 - knittl
1
无论在哪里都同意(特别是自定义断言),除了关于gdb的评论;易用性是一种习得的属性,而不是继承的属性。虽然视觉界面提供了更大范围的数据显示,但并不会本质上提供更大的效用。 - ezpz

13
多级日志系统。我发现至少有5个级别的用途:
  • 冗长:仅在调试低级别问题(如协议错误)时想看到的内容;可能被编译为发布二进制文件,因此你可以将一些不想让最终用户发现的内容放在这个级别

  • 内部:比普通级别更低级别,在频繁有用但不需要一直查看的情况下使用

  • 普通:默认输出级别,对于任何观察系统正常运行的人都有用的东西

  • 问题:程序知道如何应对的运行时错误; 你可能选择在发布版本中运行此级别,而不是 普通

  • 致命错误:像内存不足之类的“尖叫和死亡”类型消息

多级日志系统使得你可以在程序中构建大量的日志信息,而无需一直看到它们。只有在必须调试某些内容时才将日志级别调高,然后再将其设置回正常级别。在进行"printf"类型的临时调试消息时,我将它们放在普通级别,这样我就不必将日志级别调高才能看到它们,并且不会被杂乱的内部冗长级别消息所淹没。

2
我喜欢为临时消息设置一个单独的调试级别。调试级别始终显示。这样可以避免“哦,我不知道它是否会显示出来,无论如何,我只是用printf来打印这条消息”的思路。它还作为一个清晰的提醒,在调试完成后删除打印输出。 - John Kugelman

8

一般来说,printf函数非常有用。它们可以轻松地对程序进行切片,并且不需要除编辑器和编译器之外的任何工具。


9
没错,“当有疑虑时,多打印一份出来。” - Greg Hewgill
3
他们还会帮助你忽略其他出了问题的事情。在我看来,使用print来调试更适合像PHP或Python这样的语言……在C++应用程序中使用printf进行“调试”就像使用双筒望远镜进行手术一样不太合适。 - jscharf
@jscharf。我完全同意!条件断点和随时查看变量值的能力至关重要。 - rfcoder89
我不反对使用调试器,但通常我会在将问题缩小到特定区域后再使用它们。打印语句可以让您清楚地了解程序的运行情况,同时仍然让程序正常运行。 - jason0x43

6

响应措施:

  • 使用一个好的调试器,尤其是集成的调试器。使用一个能够与您的代码进行可视化交互的调试器。

  • 调试策略。熟悉您的工具。了解常见陷阱。演绎过程。使用假设和不同想法的分支。需要时返回以前的想法。跟踪您的调试进度。科学方法。

  • 分析工具(例如Dependancy Walker.NET Reflector)、内存转储、堆栈跟踪和日志。

预防措施:

  • 小型增量构建。在活动开发期间,逐步添加到您的项目并测试每个新加入的部分。不要一下子写出一堆代码然后运行并轻率地修正每个错误。逐步添加程序的每个部分,并花时间修复所有问题。尝试使每个项目增量“原子化”。

  • 多级记录,如其他人已经指出。从跟踪到致命错误等有一个严重性等级。您的生产应用程序应记录每一个操作以及其成功或失败的原因,但当不需要极端详细信息时,您也应该能够更改日志记录级别。日志应首先易于人类阅读。

  • 故障安全记录。不要依赖您的日志记录机制来处理异常情况。始终有备选方案 - 事件日志、平面文本文件、最后一道防线的电子邮件 - 以备不时之需。

  • 良好的源代码控制策略。至少按照定期时间表进行频繁的检查,以及针对任何和所有变化组进行检查,特别是广泛或可能破坏性的变化。


5

我喜欢使用gdb,通常我会在命令行模式下使用它,但是如果你不习惯这种方式,也可以使用一些图形化界面的前端工具,例如InsightDdd等。同时,日志文件也非常有用,如果你有崩溃文件,同样可以使用这些工具进行“尸检调试”。


3
我使用gdb命令commands来动态创建“printf”语句。只需确保最后一个命令是continue。
#assume two breakpoints, 1 and 2
commands 1
  silent
  echo calling baz\n
  set $print_foobar=1
  continue
end

commands 2
silent
  echo calling foobar\n
  if $print_foobar
    set $print_foobar=0
    backtrace
  end
  continue
end

我最近爱上了这种技术,因为它让我可以为已经运行的代码创建printf()语句。此外,虽然GDB脚本功能有限,但在决定要打印什么时,它可以做很多事情。到目前为止,我还没有发现一个命令部分不如printf()好或更好的情况。


2

我推荐使用gdb进行调试,特别是当你只有一个核心文件时。

当你大致知道错误出现在哪里时,printf非常好用。但是当测试团队向你反馈一个核心文件和一个奇怪的问题描述时,能够分析核心转储将会为你的调试工作提供巨大的帮助。


2

在进行多线程操作时,我无法没有日志记录。在C++中进行日志记录时,我使用templog库。它允许多个严重性(有多糟糕?)乘以多个日志目标(谁可能对此感兴趣?),并结合尽可能多的过滤日志汇(将其写入何处?),因此您不会淹没在噪音中。它通过使用模板元素来帮助编译器消除多余代码,同时不会陷入assert(do_something_important())陷阱,从而在效率方面走得更远。

另外,它非常小(我认为它分布在半打头文件中不到1kLoC),并且配有宽松的boost许可证,因此您不必依赖创建者来保持稳定。如果他们这样做-您可以自己维护它。

我只希望这些人能最终将当前代码所在的分支转换为新的主干。


1

通过跟踪,修改内存中的变量以达到某些晦涩代码分支。很少编辑并继续,由于某种原因我无法完全信任它保持健康状态,所以在更改后进行完整运行。

当无法跟踪时(例如,在Windows上使用gdb速度太慢,每次触发断点需要30秒),则使用printf。垃圾代码会扰乱多线程错误的时间,但有时这是唯一的方法。

反汇编调试器用于在没有调试信息的情况下调试发布版本(Olydbg在工作时很好用)。

适当的日志记录很好用,设置和使用需要付出努力,但在需要时非常有价值。

将崩溃的堆栈跟踪发送回家更好。

在需要的地方散布断言。

对于用户机器上的崩溃,使用Minidumps。(可重现的崩溃是最好的。如果不能依赖错误及时显示,那还能依赖什么呢?)


1
对于这个问题,我会选择有意识地构建项目,以最小化增量构建/迭代时间。作为副作用,这些步骤也是保持编辑和继续正常工作所需的步骤。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接