调试,不单纯只是发现问题,也可以帮助理解一些算法的执行流程,如快速、希尔排序、递归和回溯算法的分书问题。
要调试,就要有暂停点(因为语句执行速度太快了),也就是断点。
程序编译后就可以设置断点(在某行代码按F9,会看到一个深红色的大圆点,还多了一个黄色的箭头!这个黄色的箭头就是表示程序将要执行的语句!根据代码的逐行执行,黄色箭头相应移动。)。
并不是所有的代码行都可以设置断点,需要代码行有实际的指令动作。例如声明数据的代码并没有实际的指令动作,不能设置断点。
有了断点后,按F5键,并可将程序运行到断点。正如代码编辑、编译、连接有编辑器、编译器、连接器一样,调试也有IDE对应的一个调试器。
调试时可以调出众多类型的调试窗口,用于查看变量值、内存的数据位、堆栈函数逐层调用的情况,也可以查看对应的汇编代码,以及各寄存器的值。
我们知道,程序代码的主要模块是函数或类方法,由此,逐步调试可以逐条语句不进入函数内部进行调试,也可以逐条语句进入函数内部语句再逐语句调试。
一、常用调试命令
1.1 Insert / Remove Breakpointer (F9)
用于插入断点。
1.2 Restart (Ctrl+Shift+F5)
终止当前的调试过程,重新开始执行程序。
1.3 Stop debugging (Shift + F5)
停止调试,并执行完程序剩下的代码。
1.4 Break Execution
终止程序运行,进入调试状态,多用于终止一个死循环。
1.5 Apply code changes
当源程序在调试过程中发生改变,重新进行编译。
1.6 Show next statement
显示下一语句。
二、逐步调试
2.1 Step Into (F11)
单步执行【进入函数】,遇到子函数就进入并且继续单步执行(简而言之,进入子函数);
2.2 Step over (F10)
单步执行【跳过函数】,执行一行语句,不进入函数体内部。
在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将子函数整个执行完再停止,也就是把子函数整个作为一步。在不存在子函数的情况下是和step into效果一样的(简而言之,越过子函数,但子函数会执行)。(相对于F11,当有输出函数时,F10可以直接输出而不进入,较方便。)
2.3 Step out (shift+F11 )
从函数体内部运行出来。当单步执行到子函数内时,用step out就可以执行完子函数余下部分,并返回到上一层函数。当一个函数的代码行太多时,可以选择此命令用于执行完函数剩下的代码。
2.4 Run to cursor (Ctrl+F10)
运行到光标所在位置,如果goto语句一样。
2.6 单步执行(F11)举例
void nineNine(){ for(int i=1;i<10;++i) // ① { for(int j=1;j<=i;++j) // ② printf("%d*%d=%d",j,i,i*j); // ③ cout<
先是编译,然后① 行前插入断点(F9),运行(F5)(C文件要编译以后才可以插入断点)
可以调出watch(手工输入变量名)、variable等调试窗口(如果有函数递归调用或嵌套调用,可以调出栈窗口);
F11→③,此时如果按shift+F11,则跳出nineNine();
F11(进入printf()函数),打开窗口:find source, Please enter the path for PRINTF.C,取消,进入“Disassembly“窗口,关闭,→消失,出现|,
Shift+F11,F11……→④,F11()进入重载运算符<
最后,shift+F5(stop debug);
最后,清除断点,F9或ctrl+shift+f9
以上代码用F10调试比F11要方便,可以跳过进入printf()函数。
三、各类调试窗口
对于一个有“问题”待分析的程序,设置断点的目的是为了追溯程序的执行过程,跟踪程序的动态执行过程,从而排查错误来解决问题。所以实际开发工作中,对于一些较复杂的程序,我们很多情况下的断点跟踪是为了获得某一断点情况下的程序状态,这个状态就包括程序中的变量、返回值、判断条件等内容,这些内容可以通过调试器提供的监视窗口来获取。
3.1 QuickWatch
快速查看变量或表达式的值。
3.2 Watch 观察窗口 Alt + 3
用于观察指定变量或表达式的值。
Watch监视区,可以输入变量名,表达式,也可以在变量名前加&,用于输出内存地址值。
3.3 Variables 变量窗口 Alt + 4
用于观察断点处或其附近的变量的值。
3.4 Register寄存器窗口 Alt + 5
用于观察当前运行点各寄存器的值。
3.5 Memory 内存窗口 Alt + 6
用于观察指定内存地址的内容。内存监视窗口有一个输入地址的文本框,地址可以是直接通过取值运算符&取出的地址,也可以是变量名,输入后回车,内存分布即可跑到该地址处。内存值以16进制表示,两个16进制位表示一个内存位byte。
3.6 Call Stack 调用栈窗口 Alt + 7
用于观察调用栈中还未返回的被调用函数列表,当前函数位于最上边。对于理解递归递用及有多层次函数调用时,可以清晰地看到逐层的函数调用关系。
3.7 Disassembly 反汇编窗口 Alt + 8
用来查看程序语句与汇编语句的对应关系,汇编语句中就包含各寄存器的情况。
例如一个累加求和程序,反汇编窗口可以结合寄存器窗口查看:
可以看到变量i和sum在分别存放于EAX和ECX两个通用寄存器中。
反汇编窗口调出后,按F11,可以按汇编逐步查看执行情况,并结合寄存器窗口,查看各寄存器的值的动态变化。
四 编辑断点 Alt+F9
4.1 条件断点
在写程序调试过程中,除了可以在有命令执行动作的任意行(数据声明除外)下断点以外,由于程序实际执行的复杂性,处于便捷和更加精准的需求,VC为我们还提供了条件断点,即满足某一条件时才触发断点。
举一个具体场景,比如当程序进行循环等大量运算时,如果单步调试显然会相当费事,而条件断点就可以很好解决这个问题。
如打印九九乘法表程序,代码如下:
比如我们想在打印8*9=72这个结果时候断点停止,如果单步运行显然要按70多下F10… 这个时候无疑条件断点将是很好的选择!具体如下:
首先在printf输入语句处F9下断点!(注意这个断点一定要在i和j在满足8和9的时候可以触发处,不然即使条件满足也将无法触发断点。)
然后点击Edit – 断点 或者按快捷键 Alt+F9,弹出断点设置框,如下图:
这个时候,在下方断点处已经有一条刚刚F9设置的断点,可以看到位于第十行,单击这条断点信息,在分隔符处会自动加载,并且条件按钮这个button也可以点击了,如下图:
继续点击条件button,在回车表达式的编辑框内输入条件:“i==9 && j==8”(这里还是C语言语法,不带双引号)
点击确定关闭对话框!注意观察断点的条件也已经进行了更新。
OK,这个时候按F5运行程序!可以观察到控制台的输出和当前i与j的值均为条件里的要求。
4.2 内存断点
内存断点就是在一个地址处下断点,这个地址只要有读写就断住,就这么简单。那么下来我们给大家演示一下,先看一个程序:
#include int main(){int a=3, b=25;int t;t=a;a=b;b=t;b=a;printf("a=%d,b=%d",a,b);return 0;}
想看a变量地址出的改变情况,就可以用内存断点来下断点,我们先查看&a的地址,比如笔者这里找到是0x0012FF44,那么现在就可以下断点了,方法是:
点击Edit→断点→data选项卡,在编辑框里输入想要下的断点处地址0x0012FF44(注意前面一点要有0x才是有效的16进制地址)。
然后排列结构这里填4,因为int占4个字节。
只要0x0012FF44被读写都会引发断点,并弹出以下对话框(多次有读写会多次弹出)!
我们还可以跟踪这片内存的变化情况。
五 其它
Ctrl+Shift+F10 将调试的断点移动到光标处。
ctrl+shift+f9 清除所有断点。
六 跟踪递归和回溯流程(分书问题)
-End-