创作不易,点个关注吧
目录
一. 前言
二. 预备知识
2.1 Debug版本和Release版本的区别
2.2 常用的调试快捷键:
2.2.1 与调试语句执行相关的快捷键
2.2.2 与窗口相关的快捷键
三. 调试过程方法及实用调试技巧
3.1 程序调试的基本过程
3.2 调试过程的执行和相关快捷键的介绍
3.2.1 F5、F9快捷键键及断点的介绍
3.2.2 快捷键F10和F11的使用方法
3.3 调试过程中常用窗口的介绍
3.3.1 监视窗口
3.3.2 自动监视窗口
3.3.3 局部变量监视窗口
3.3.5 调用堆栈的监视
3.3.6 寄存器监视窗口
3.3.7 转到反汇编操作
四. 调试案例的演示(数组越界访问打印死循环的问题)
五. 总结
调试作为程序设计中不可或缺的环节,对于发现程序中存在的bug具有不可替代的作用。无论多么优秀的程序员,都不可能保证代码100%正确,都需要经历调试代码。优秀的程序员,一定也是出色的调试大师。
对于程序设计的初学者,在程序出现bug时,往往会采取类似忙猜的方法寻找问题,这就是所谓的迷信式找bug,觉得某处不对就改改,运行结果不正确就换个方法再改改,直到程序运行正确,但这样即使程序正确执行程序员也往往无法得知问题到底出在哪里。
程序调试的过程,就是找茬、纠错的过程
<1> Debug版本:通常成为调试版本。它包含调试信息,并且不进行任何优化,便于程序员调试。
<2> Release版本:称为发布版本。程序已不存在问题,编为可执行文件,卖给客户。Release版本往往对程序文件进行了各种优化,使得程序代码在大小和运行速度上都是最优的。
进行调试一定要在Debug版本下进行。
图2.1 Debug版本和Release版本的选择快捷键 | 功能 |
---|---|
F5 | 启动调试,经常用于直接跳到下一个断点。 |
F9 | 创建和取消断点,通常配合F5使用 |
F10 | 逐过程调试代码,一个过程可以是一个函数或者一条语句,也可用于启动调试,但F10不能进入到函数内部。 |
F11 | 逐语句调试,可以进入到函数内部。 |
CTRL + F5 | 直接启动运行程序,不调试。 |
Shift + F5 | 停止调试。 |
CTRL + Shift +F5 | 重启调试。 |
快捷键 | 功能 |
---|---|
CTRL + ALT + W | 启动监视窗口,先同时按下CTRL+ALT+W,再按下1或2或3或4,分别对应启动监视窗口1 2 3 4。 |
CTRL + ALT + V,A | 先按下CTRL+ALT+V,再按下A,启动自动监视窗口。 |
CTRL + ALT + V,L | 先按下CTRL+ALT+V,再按下A,启动局部变量监视窗口。 |
CTRL + ALT + C | 启动调用堆栈监视窗口。 |
CTRL + ALT + U | 启动模块监视窗口。 |
CTRL + ALT + Z | 启动程序进程监视窗口。 |
CTRL + ALT + D | 转到反汇编代码。 |
CTRL + ALT + G | 启动寄存器监视窗口 |
<1> 发现程序错误的存在
<2> 以隔离、消除等方法对错误进行定位
<3> 确定错误产生的原因
<4> 提出纠正错误的方法
<5> 纠正程序,重新测试,直到程序正常运行
由上文可知,快捷键F9的作用是设置和取消断点,F5的作用是启动调试,在程序调试过程中,每按下一次F5,就执行两个断点之间的内容。
在开始调试前,需要先在合适的位置设置断点,如图3.1所示,将鼠标定位在第9行,按下F9快捷键,创建断点,如图3,2所示再以同样的方式在第15行创建断点。
按下F5快捷键开始调试,如图3.3所示,程序在按下F5后先执行到第9行(第一个断点处),如图3.4所示,再次按下F5,程序执行到断点2处。断点2之后没有断点,再次按下F5程序全部运行完成,调试结束。
若在后来的调试过程中发现某个断点多余,只需要将鼠标定位至想要消除的断点所在的行,按下快捷键F9即可,或是直接鼠标左键单机程序左侧的红点(断点)来消除断点。
(重点)条件断点的设置:
在实际的程序设计中,如果我们定义了一个可以进行10次的循环,而我们认为在执行到第五次循环时可能出现了bug,如果我们一条指令一条指令的执行,则会浪费大量时间,效率低下。我们此时希望按下F5后直接跳到第5层循环,这里就用到了条件断点。
设置条件断点的操作十分简单:
STEP1:鼠标右击断点,选择条件选型。
STEP2:设置断点条件(i == 5)
图3.6 创建条件断点的第一步 图3.7 创建条件断点的第二步上文说到快捷键F10是逐过程调试程序,不能进入到函数内部,而F11是逐语句调试程序,可以进入到函数内部。对下面通过函数实现两数相加的程序的代码的调试,说明F10与F11的区别
#include
int sum(int a, int b)
{
int z = 0;
z = a + b;
return z;
}
int main()
{
int a = 3;
int b = 5;
int c = sum(a, b);
printf("%d\n", c);
return 0;
}
上文提到按下F10也可以开始调试,那么这里我按下F10开始调试,在调试过程中一直按F10,每按一次程序执行一行。注意:如图3.8和3.9所示,当程序执行至调用函数命令所在的第15行时,若按下F10,则可以看到,程序直接运行到了下一行,也就是说一次性执行完整个函数,没有进入到函数内部调试。
如图3.10和3.11所示若执行到调用sum函数命令所在的行时按下F11,则进入到sum函数内部进行调试。
监视窗口实现的功能是实时监视变量在程序执行过程中的变化情况。若要开启监视窗口,首先要先进入调试状态,在菜单栏中依次选择:调试 -> 窗口 -> 监视 ->监视1 2 3 4中任意一个,监视1 2 3 4所实现的功能是相同的,不必区分。
我们可以在监视窗口中输入自己希望监视的变量,可以清楚地观察到变量内容随着程序调试过程执行的变化情况。
开启自动监视窗口的操作:进入调试界面 -> 选择菜单栏的调试 -> 窗口 -> 自动窗口
自动窗口不需要人工输入需要监视的变量,随着调试过程的执行,自动窗口会自主判断要去监视那些变量,会实时创建和销毁窗口中监视的内容。一般来说,自动监视窗口很少使用。
如图3.16所示,程序调试至14行,到此为止创建了a、b两个局部变量,自动窗口出现了a和b。
如图3.17所示,随着调试进入到函数内部,取消了对实参a、b的监视,对形参x、y开始监视。
图3.17 调试进入到自定义函数内部后自动窗口的变化如图3.18所示,离开自定义函数sum后,形参销毁,取消了对形参x、y的监视,重新开始对a、b进行监视。
由上述可见,自动窗口自主确定要进行监视的变量,灵活性低,很多时候不能满足程序员的需求,故一般不推荐使用。
依次选择:调试 -> 窗口 -> 局部变量,打开局部变量监视窗口。
图3.18和3.19分别展示了在主函数内调试时和进入自定义函数内部调试时局部变量窗口的情况。可以看出,局部变量窗口对调试过程所在的大括号内部的局部变量进行了监视。
图3.19 在主函数内调试时局部变量窗口的情况如下程序和图3.21所示,函数栈帧开辟和销毁的情况可以在调用堆栈窗口中被监视,函数栈帧具体是如何创建和销毁的,详细介绍可以参考我之前的文章《函数栈帧的创建和销毁》(https://blog.csdn.net/weixin_43908419/article/details/127339425?spm=1001.2014.3001.5502),在该程序执行过程中,main函数、test函数、test2函数依次在内存中开辟栈帧。
#define _CRT_SECURE_NO_WARNINGS 1
#include
void test2() //定义test2函数
{
int n = 5;
printf("%d\n", n);
}
void test() //定义test函数
{
test2(); //调用test2函数
}
int main()
{
test( ); //调用test函数
return 0;
}
可以用于监视每个寄存器中存放了那些内容,具体每个寄存器的功能作用也可以参考我之前的文章《函数栈帧的创建和销毁》。
转到反汇编可以观察C语言代码对应的汇编语言,有两种方法可以转到反汇编:
<1> 进入调试界面后,依次选择:调试 -> 窗口 -> 反汇编,如图3.23所示。
<2> 直接在调试界面中单机鼠标右键,选择转到反汇编,如图3.24所示。
如图4.1所示的程序会死循环打印hehe。但是,这里我们通过限制for循环的条件i<=12,仅希望进行13层循环,程序显然在执行死循环,至于这是为什么,这里通过调试来寻找原因:
如图4.12所示,程序此时越界访问至arr[12]所在的地址,即将执行arr[i]=0命令。此时按下F10,将arr[12]所在的地址对应的内容改为0,如图4.3所示,i被赋值为0,相当于回到了循环最开始执行的位置。
置于为什么会出现这种情况,我猜测是因为i在内存中所占的地址为arr[12]对应的地址,为此,我在监视窗口中输入&arr[12]和&i,可以看到arr[12]和i的地址相同。
综上,该程序打印死循环的原因是:
数组越界访问并赋值,arr[12]的地址和i的地址相同,在越界访问中更改了i的值,使循环回到了初始位置,每次越界访问到arr[12],i均被赋值0,故程序死循环执行。
图4.3 按下F10,i被赋值为0出现这种情况,看似是巧合,但也不完全是,可以从以下两方面来解释原因:
<1> i 和 arr 均为局部变量,局部变量放在栈区上。栈的使用习惯是:先用高地址,再用低地址。
<2> 数组随着下标的增长地址由低到高变化。
图4.5展示了数组 arr 和整型变量 i 在内存中的占用情况。
由于栈区的使用习惯是先用高地址后用低地址,则如果先定义整型数组arr,再定义局部变量 i 就不会出现打印死循环的问题。此时程序会报错崩溃,提示越界访问数组。
本文介绍了VS2019的实用调试技巧及相关操作的具体执行流程,通过使用监视窗口,可以在调试的过程中实时监视想要观察的变量或是调用堆栈、寄存器等。除此之外,本文在最后以案例的方法展示了调试的重要性。