作者:小树苗渴望变成参天大树
作者宣言:认真写好每一篇博客
作者gitee:link
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
今天我们来讲一下实用调试技巧,主要在vs2019上进行调试,可以说一个合格的程序员指挥写代码时不够的,还需要会调试,新手程序员可能百分之80的时间的时间在写代码,百分之20的时间在调试,而大佬却截然相反,因为调试太重要了,我们只会写不知道怎么去修改我们出现bug的代码怎么行呢??所以今天我讲手把手带你们调试,让你们知道调试是什么,怎么去调试,话不多说,我们接入正文。
我将从一下几个方面重点去介绍:
1.什么是bug?
2.调试是什么?有多重要?
3.debug和release的介绍。
4.windows环境调试介绍。
5.一些调试的实例。
6.如何写出好(易于调试)的代码。
7.编程常见的错误。
bug其实是臭虫的意思,话说在之前,我们的电脑是很大一台,跟房间差不多一样大,有一天工作人员发现电脑不工作了,就与检查电脑,查了很长时间,在一个极管上发现了一个已经死掉的臭虫,后来换了一个新的极管,电脑整除工作,于是工作人员就将这个臭虫粘贴在当天的日志上,这只臭虫就被当成第一个是计算机不能工作的bug!!!也可以说是第一个计算机程序错误。
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
一名优秀的程序员是一名出色的侦探。
这是两种非常有意思的图片,这也就是程序员为什么需要多动脑的原因。我们要拒绝迷信编程,不要去质疑计算机,所以我们要去调试才能知道我们错哪里了,不能盲目的去找错误
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
软件是什么样子的?
我们的公司有产品经理-需求-设计产品,会生成产品文档,程序员会根据产品的要求去开发软件,测是人员也是根据产品的去要求去测试软件通过这两关软件才能正式发布。
1.发现程序错误的存在
(1)程序员自己发现bug。
(2)测试人员自己发现bug。
(3)用户发现bug(这时候后果就严重了)。
2.以隔离、消除等方式对错误进行定位
(1)我们可以通过屏蔽一些代码和修改一些代码来大致判断代码bug错在什么地方。
3.确定错误产生的原因
(1)我们可以看题目要求和产品要求来确定为什么会出现错误。
4.提出纠正错误的解决办法
(1)我们应该怎么去修改这个bug,通过测试人员和程序员自己去弄出解决办法。
5.对程序错误予以改正,重新测试
(1)我们修改好bug之后需要再次去测试自己的代码是否符合要求,不行再次去修改bug。
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
在vs下我们就会有这两个版本:
我们来看看两个的区别在哪里??
我们在上面说过Release是一个版本的优化,所以我们看到的Release版本的文件夹大小比Debug版本的小
Debug和Release反汇编展示对比:
看不懂没关系,虽然在两个版本下都可以进行调试,一个是在原有的基础下调试,一个是在优化后的调试两者调试想要达到我们的结果也是不一样的,为什么这么说呢?
看一个案例:
#include
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
这个代码看上去就是越界访问了
如果是 debug 模式去编译,程序的结果是死循环。
如果是 release 模式去编译,程序没有死循环。
那他们之间有什么区别呢?
就是因为优化导致的。
这个代码我会在这篇文章中后期给大家讲解两个版本调试的区别
对于我们初学者来说我们一开始基本上都是在Windows上进行写代码,所以我将Windows环境来介绍调试.刚才讲了Debug和release两个版本,我们也对这两个版本有了更深的了解,所以我们将在Debug版本下给大家介绍调试技巧
注:linux开发环境调试工具是gdb,这里我就不做具体介绍了
这是我们没有进行调试时看到的调试窗口,可以说很少,那我们看看调试开始后的样子
注:我们想要使用这些调试窗口必须是在调试开始后才能看到,不要还没有开始调试就说自己为啥没有这些窗口
最常使用的几个快捷键:
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点。
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最
长用的)。
CTRL + F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
F5一般和F9一起使用,我们来看案例:
有的电脑可能需要配合Fn键使用,我们可以看到我们点了F5程序调试了起来,直接来到了第一个断点,但到了第二个断点的时候,我们在使用F5却没有直接跳到第三个断点,这是为什么呢?
原因是每一次循环都要跳到for那一条语句进行判断,进入循环又遇到了断点,所以我们会看到在第二个断点的时候并每一有一次性跳到第三个断点
断点也可以跨文件使用,并且我们点击旁边灰色的地方也可以设置断点
但是断点在代码量少的前提上,体现不出来他的有点,当代码量足够多,才能体会断点的好处
F10是逐过程
我们可以看到逐过程是可以一次函数调用,或者是一条语句。但这样不太好,我们不能看到函数内部的变化,所以我们就又F11,逐语句,执行每一条语句.
F11是逐语句
我们可以看到按F11我们就可以进入函数内部去观察函数,使我们很方便的去观察每一条语句的变化情况,但是对于vs,不支持F11进入库函数.
CTRL F5
这就是直接运行程序,不调试!!!
如果你想要了解其他的快捷键,我们可以在后面看到
常用快捷键我就先讲到这里为止。
在调试开始之后,用于观察变量的值。
我们有三个窗口可以看临时变量的值,自动窗口,局部变量窗口,监视窗口。
1.自动窗口
我们先点开调试,在点窗口,就能看到我们的自动窗口:
我们可以看到自动窗口会根据程序自己的变化来任意的删除和增加一些变量的值让你进行观察
2.局部变量窗口
我们仔细看到,相同的代码在两个窗口上的变化是不同的,局部变量的窗口只能看到当前所在代码块的局部变量,出了代码块局部变量就自己删除,而且只能看到局部变量,看到&a,&b这些值。
但是这两种查看的方法不好,因为不能自己增加或者删除想要的值,我们进入函数内部,只能看到内部的变量(x,y),看不到函数外部的变量(a,b等),不能够随心所欲,所以我闷接下来将讲到监视窗口。
3.监视窗口
我们监视一共有四个窗口,随便点开哪一个去观察自己想要的值都是可以的
我们可以清晰的看到我们想观察什么变量的值就观察什么变量的值,随心所欲,所以这也是最常用的窗口之一。
在调试开始之后,用于观察内存信息。内存也有四个窗口,并且相互独立,想要使用哪个就使用哪个
栈是一个线性的结构,他的特点是先进后出
可以这么说程序都是由函数组成,函数一层调用一层,构成整个程序,在使用main()函数的时候,他也是别的函数经过多次调用才到main()函数。
我们来演示一下:
我们可以看到函数的调用关系,在代码量少的情况下体现不出来,但在代码量多的情况就体现了他的优点。通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置
补充一点:你们看到的可能是这个,需要把显示外部代码点开才能看到我刚才的界面
接下来的两个窗口目前用处不太大,我们先了解一下吧
在调试开始之后,有两种方式转到汇编:
(1)第一种方式:右击鼠标,选择【转到反汇编】:
(2)第二种方式:
当我们学到汇编语言的时候我们再来看里面的语言会理解一些。
我们可以看到一些寄存器里面的值,如果你知道寄存器里面这些符号可以在监视里面查看,但要切到16进制数去查看。
1.一定要熟练掌握调试技巧。
2.初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写程序,但是80%的时间在调试。
3.我们所讲的都是一些简单的调试。
4.以后可能会出现很复杂调试场景:多线程程序的调试等。
5.多多使用快捷键,提升效率
实现代码:求 1!+2!+3! …+ n! ;不考虑溢出
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for(i=1; i<=n; i++)//计算每一位数的阶乘
{
for(j=1; j<=i; j++)//计算数的阶乘
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
这里我们输入3,期待输出9,但实际输出的是15。
why?
这里我们就得找我们问题。
那我们怎么去解决呢?
我们在第一个循环和第二个循环之间加一个赋值的操作,使得ret每次都是从1开始计算的。
这才是我们想要的结果。这就是调试的好处。
这是一个只在特定的环境下才会出现的情况,让我们来看看他的真面目吧。
#include
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
我们来看看两个版本下情况是截然不同的。哪为什么是这样的,我们通过调试来看看为什么会出现这样的情况。
我们在Debug环境下调试看看:
我们可以清晰的看到我们在第十二次循环结束后,i突然变成了1,哪让我们来观察i和arr[12]的地址是怎么样的?
这样才造成死循环,希望大家能够理解。
哪恰好i就是等于arr[12]的地址?
我们来重点介绍,我们常说的内存分为三个部分,栈区,堆区,静态区,而局部变量是在内存的栈区存放的,栈区的使用习惯是,先使用高地址,在使用低地址,我们画一个图让大家理解
由此可知,arr[10].arr[11]的地址是随机值,arr[12]恰好是i的地址,改变arr[12]就是改变i的值,所以死循环了,在vs2019 Debug版本下,iarr数组中间刚好就差两个地址。在vc6.0环境下,之间没有空间(i<=10),在gcc之间有一个整型的空间(i<=11),大家可以下去测试一下,
哪就有人会想可不可以把int i;定义放到定义数组下面,哪这样不就解决问题了吗?答案可以是可以,但是会出现下面这种情况
1.虽然这么创建变量是可以解决死循环,但是没必要,因为变量的创建是任意地方,这样就加了限制
2.会出现数组越界的报错信息。
这样问题来了,第一种为什么会出现死循环,却没有产生报错的信息,他也是数组越界访问啊,原因是程序忙着死循环,根本没有机会产生保存的机会。
我们在release版本下看看看为什莫不会出现死循环
因为在release版本下做了优化,导致了我们i的地址在arr地址的下面,所以不会出现死循环。
7.1 编译型错误
直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
7.2 链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
7.3 运行时错误
借助调试,逐步定位问题。最难搞。
我今天所演示的调试都是运行时错误。
关于本次使用调试技巧,今天就将到这里,我们今天所有的环境都在Win11 vs2019 32位机器下 Debug版本下的调试,特别注意的是我们调试起来之后才能看到那些窗口了,所以我们要多去使用调试,才能如鱼得水。相信你们在进行编程的时候不要指挥写代码不会去调试,那我们下期再见,也感谢各位粉丝的支持,你们的支持才是我的动力。