今天发文晚了,抱歉.
其实看一个会写程序的人功力怎么样,我觉得看ta的键盘F10,F11就能知道,如果磨得没有字了,或者油腻腻的(别问我为什么是油腻腻的这个形容词).因为程序写好运行的时候,就像一个个小工厂一样.有条不紊的全力运行,某种意义是打断就不没了,因为动起来本身就是一种生命力.上个月看一本讲操作系统的书,里面有个大佬说,操作系统一旦动起来连它的设计者都搞不清它内部在时空上每一刻的详细状态(大致意思就是这样,我也没记).
说了一个什么问题,我觉得是描述了一个复杂性的问题以及规模问题,既然详细把握不到,就缩小一些.看局部,因此有了各种系统级调试的奇技淫巧.但是核心做法无非就是"窥探"各种变量的"来世今生".扯得远了,对于程序讲也是,调试其实也是去追踪各种变量(流淌得数据)得"前世今生".
首先讲一下调试过程:
为什么要用VC讲,其实无所谓这个东西.调试学的是一种思想,要是拘泥于工具,那换个环境就懵逼,你学了个锤子.
一个程序扑街的原因有很多,比如下面这些:
语法错误:在编写程序时违反了 C 语言的语法规定。语法不正确、关键词拼错、标点漏写、数据运算类型不匹配、括号不配对等都属于语法错误,在进入程序编译阶段,编译系统会给出出错行和相应“出错信息”。我们可以双击错误提示行,将光标快速定位到出错代码所在的出错行上。根据错误提示修改源程序,排除错误。 (快捷键看之前的文章)
连接错误:如果使用了错误的函数调用,比如书写了错误的函数名或不存在的函数名,编译系统在对其进行连接时便会发现这一错误。纠正方法同上。
逻辑错误:虽然程序不存在上述两种错误,但程序运行结果就是与预期效果不符。逻辑错误往往是因为程序采用的算法有问题,或编写的程序逻辑与算法不完全吻合。逻辑错误比语法错误更难排除,需要程序员对程序逐步调试,检测循环、分支调用是否正确,变量值是否按照预期产生变化。
运行错误:程序不存在上述错误,但运行结果时对时错。运行错误往往是由于程序的容错性不高,可能在设计时仅考虑了一部分数据的情况,对于其他数据就不能适用了。例如打开文件时没有检测打开是否成功就开始对文件进行读写,结果程序运行时,如果文件能够顺利打开,程序运行正确,反之则程序运行出错。要避免这种类型的错误,需要对程序反复测试,完备算法,使程序能够适应各种情况的数据。
为了方便程序员排除程序中的逻辑错误,VC 提供了强大的调试功能。每当我们创建一个新的 VC 工程项目时,默认状态就是 Debug(调试)版本。调试版本会执行编译命令_D_DEBUG,将头文件的调试语句 ifdef 分支代码添加到可执行文件中;同时加入的调试信息可以让开发人员观察变量,单步执行程序。由于调试版本包含了大量信息,所以生成的 Debug 版本可执行文件容量会远远大于Release(发行)版本。
开始进入正题:
断点
断点是调试器设置的一个代码位置。当程序运行到断点时,程序中断执行,回到调试器。断点是最常用的技巧。调试时,只有设置了断点并使程序回到调试器,才能对程序进行在线调试。
VC 可以在程序中设置断点,跟踪程序实际执行流程。设置断点后,可以按“F5”功能键启动 Debug 模式,程序会在断点处停止。我们可以接着单步执行程序,观察各变量的值如何变化,确认程序是否按照设想的方式运行。设置断点的方法是:将光标停在要被暂停的那一行,选择“Build MiniBar”工具栏按钮“Insert/Remove Breakpoint (F9)”按钮添加断点,断点所在代码行的最左边出现了一个深红色的实心圆点,这表示断点设置成功。
如果该行已经设置了断点,那么再次按“F9”功能键会清除该断点。
条件断点:
可以为断点设置一个条件,这样的断点称为条件断点。对于新加的断点,可以单击Conditions按钮,为断点设置一个表达式。当这个表达式发生改变时,程序就 被中断。底下设置包括“观察数组或者结构的元素个数”,似乎可以设置一个指针所指向的内存区的大小,但是我设置一个比较的值但是改动 范围之外的内存区似乎也导致断点起效。最后一个设置可以让程序先执行多少次然后才到达断点。
数据断点:
数据断点只能在Breakpoints对话框中设置。选择“Data”页,就显示了设置数据断点的对话框。在编辑框中输入一个表达式,当这个 表达式的值发生变化时,数据断点就到达。一般情况下,这个表达式应该由运算符和全局变量构成,例如:在编辑框中输入 x这个全局变量的名字,那么当程序中有x= !x时,程序就将停在这个语句处。
消息断点:
VC也支持对Windows消息进行截获。他有两种方式进行截获:窗口消息处理函数和特定消息中断。
在Breakpoints对话框中选择Messages页,就可以设置消息断点。如果在上面那个对话框中写入消息处理函数的名字,那么 每次消息被这个函数处理,断点就到达(我觉得如果采用普通断点在这个函数中截获,效果应该一样)。如果在底下的下拉 列表框选择一个消息,则每次这种消息到达,程序就中断。
调试命令(进程控制):
我们也可以在 VC“Build”(组建)菜单下的“Start Debug”(开始调试)中点击 Go(F5)命令进入调试状态,Build 菜单自动变成 Debug 菜单,提供以下专用的调试命令: Go(F5) 从当前语句开始运行程序,直到程序结束或断点处。 Step Into(F11) 单步执行下条语句,并跟踪遇到的函数。 Step Over(F10) 单步执行(跳过所调用的函数) Run to Cursor(Ctrl+F10) 运行程序到光标所在的代码行。 Step out(Shift+F11) 执行函数调用外的语句,并终止在函数调用语句处。 Stop Debugging(Shift+F5) 停止调试,返回正常的编辑状态
必须在运行程序时用 Go 命令(而不是 Execute)才能启动调试模式。在调试模式下,程序停止在某条语句,该条语句左边就会出现一个黄色的小箭头。我们随时中断程序、单步执行、查看变量、检查调用情况。比如,按“F5”功能键进入调试模式,程序运行到断点处暂停;不断按“F10”功能键,接着一行一行地执行程序,直到程序运行结束。
需要说明的是,如果希望能一句一句地单步调试程序,在编写程序时就必须一行只写一条语句
单步调试程序的过程中,我们可以在下方的Variables (变量)子窗口和Watch(监视) 子窗口中动态地察看变量的值,Variables 子窗口中自动显示当前运行上下文中的各个变量的值变量,而 Watch 子窗口内只显示在此 Watch 子窗口输入的变量或表达式的值。随着程序的逐步运行,也可以直接用鼠标指向程序中变量查看其值。程序会为自动型变量分配了内存,但它们的初始值是随机的。
Variables 子窗口有 3 个选项卡:
Variables 子窗口有 3 个选项卡:Auto、Locals 和 This。
Auto 选项卡:显示出当前语句和上一条语句使用的变量,它还显示使用 Step over 或 Step out 命令后函数的返回值。
Locals 选项卡:显示出当前函数使用的局部变量。
This 选项卡:显示出由 This 所指向的对象(C 语言不用 this)。
如果变量较多,自动显示的Variables 窗口难以查看时,还可以在右边的Watch 子窗口中添加想要监控的变量名。Watch1 子窗口中添加了变量。我们还可以直接将变量拖动到 Watch 子窗口的空白 Name 框中。添加结束后,该变量的值会被显示出来。并且随着单步调试的进行,会看到变量的值逐渐变化。如果各变量的值按照设想的方式逐渐变化,程序运行结果无误,本次开发就顺利结束了。如果发现各变量值的变化和设想的不一致,说明程序存在逻辑错误,那就需要停止调试,返回编辑窗口,查错并修改程序。
查看内存:
数组和指针指向了一段连续的内存中的若干个数据。可以使用 memory 功能显示数组和指针指向的连续内存中的内容。在 Debug 工具条上点 memory 按钮,弹出一个对话框,在其中输入数组或指针的地址,就可以显示该地址指向的内存的内容。
Watch:
VC支持查看变量、表达式和内存的值。所有这些观察都必须是在断点中断的情况下进行。
观看变量的值最简单,当断点到达时,把光标移动到这个变量上,停留一会就可以看到变量的值。
VC提供一种被成为Watch的机制来观看变量和表达式的值。在断点状态下,在变量上单击右键,选择Quick Watch, 就弹出一个对话框,显示这个变量的值。
单击Debug工具条上的Watch按钮,就出现一个Watch视图(Watch1,Watch2,Watch3,Watch4),在该视图中输入变量或者表达式,就可以观察 变量或者表达式的值。注意:这个表达式不能有副作用,例如++运算符绝对禁止用于这个表达式中,因为这个运算符将修改变量的值,导致 软件的逻辑被破坏。
Memory:
由于指针指向的数组,Watch只能显示第一个元素的值。为了显示数组的后续内容,或者要显示一片内存的内容,可以使用memory功能。在 Debug工具条上点memory按钮,就弹出一个对话框,在其中输入地址,就可以显示该地址指向的内存的内容。
寄存器:
Debug工具条上的Reigsters按钮弹出一个框,显示当前的所有寄存器的值。
CallStack
调用堆栈反映了当前断点处函数是被那些函数按照什么顺序调用的。单击Debug工具条上的Call stack就显示Call Stack对话框。在CallStack对话框中显示了一个调用系列,最上面的是当前函数,往下依次是调用函数的上级函数。单击这些函数名可以跳到对应的函数中去。
其他调试手段
系统提供一系列特殊的函数或者宏来处理Debug版本相关的信息,如下:
宏名/函数名 说明
TRACE 使用方法和printf完全一致,他在output框中输出调试信息
ASSERT 它接收一个表达式,如果这个表达式为TRUE,则无动作,否则中断当前程序执行。对于系统中出现这个宏 导致的中断,应该认为你的函数调用未能满足系统的调用此函数的前提条件。例如,对于一个还没有创建的窗口调用SetWindowText等。
VERIFY 和ASSERT功能类似,所不同的是,在Release版本中,ASSERT不计算输入的表达式的值,而VERIFY计算表达式的值.
附:调试时需要注意的事情。感觉挺好,特记录下来。
(另,“print”和“重新阅读代码并思考”是很重要的方法。确实,有时候调试工具的单步调试会让你局限于细节,而没有从整体上去观察思考代码。不过 有时候调试工具也能给我们带来很大帮助。也许两者结合起来会让调试更加有效率吧)
1. 自变量顺序错误。(注意参数命名,以避免颠倒顺序。实参和形参用相同的名字会调理清晰)
2. 拼写错误。
3. 忘记初始化。
4. 对象与值相等。“==” 与" = "
5. 别名。数组、链表的深度复制和浅复制。
6. 副作用。函数执行过程可能会改变一些变量的值。
7. 收集自己经常犯的错误,调试时先从易犯的错误下手。
8. 记录你尝试过的修改,调试用的“print”可以注释掉而不是删除。
9. 调试别人代码的时候,调试的是代码,而不是注释。不要被注释所迷惑。
10. 寻求帮助。旁观者清,寻找别人帮助,尽可能向别人解释清楚自己的程序,也许你在解释的过程中就能发现错误了。
11. 清醒一下大脑。
12. 欲速则不达。考虑好修改方案,而不是急功近利。修改这个bug的过程可能会产生更多的bug。
13. 代码不能总是变长。代码写的越多,出错误的可能就越大。当你遇到问题时,试着把你的代码整理一下,整理的过程中也许你就可能找到错误。
14. 及时备份旧版本代码。确保你的代码能够回到Debug前。没有什么比你Debug 4个小时,最后发现还没有4个小时前好,更令人沮丧的是你不能回到最开始的状态。硬盘空间很廉价,多保存一下旧版本的代码绝对没有坏处。
另外这些都是个人对调试的粗浅理解,如有建议,欢迎拍砖.