1. bug的概念
bug的本意是 “虫子” 和 “臭虫”,但是现在⼀般泛指电脑系统或程序中,隐藏着的⼀些未被发现的缺陷或问题,简称程序漏洞。
bug一词的发明过程是非常有趣的,它最早来源于一位美国海军电脑专家——格蕾丝·赫柏(Grace Murray Hopper)。上世纪40年代,计算机还是由继电器和真空管驱动的,机器有房间那么大。体现当时技术水平的MarkII,是由哈佛大学制造的一个庞然大物。1947年9月9日,格蕾丝·赫柏对Harvard Mark II设置好17000个继电器进行编程后,对整机进行运行时,它突然停了工作。技术人员爬上去找原因,发现这台巨⼤的计算机内部⼀组继电器的触点之间有⼀只⻜蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。所以在报告中,赫柏⽤胶条贴上飞蛾,并用 “bug” 来表示 “⼀个在电脑程序里的错误”,“bug” 这个说法也⼀直沿用到了今天。
![](https://img-blog.csdnimg.cn/ef5b30cfc0ef4c408c56adaf4209ab63.jpeg)
2. 调试的概念
当我们发现程序中存在的问题的时候,那下⼀步就是找到问题,并修复问题。而找到问题的过程就被称为调试,英文叫debug (解决问题的意思) 。
调试一般分为以下几个步骤:
1、发现程序错误的存在
2、以隔离、消除等但是去定位问题的位置
3、确定错误产生的原因
4、提出纠正错误的解决办法
5、对程序错误予以改正,重新测试
下面放一组漫画,让大家直观感受一下代码的调试过程。没错,这也是本人敲代码的状态😥。
![](https://img-blog.csdnimg.cn/b2a6a8a0bfe6437eb60b70b49fd7ff62.gif)
3. debug和release
![](https://img-blog.csdnimg.cn/7e39567464c949d981b81c5de951d8e2.png)
在VS编译器上编写代码的时候,总能看到有debug和release两个选项,分别是什么意思呢?
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序;程序员在写代码的时候,需要经常性的调试代码,就将这里设置为debug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运⾏速度上都是最优的,以便用户很好地使用。当程序员写完代码,测试再对程序进行测试,直到程序员的质量符合交付给用户使用的标准,这个时候就会设置为release,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息等。
debug版本
![]()
release版本
![]()
通过上述两个图片,我们可以直观的看出,两个不同版本的可执行文件的大小差异很大,debug版本要比release版本大得多。
由于各自的特性,两种版本都有相应的应用场景:
Debug版本什么时候用?——程序员写代码的时候。
Release版本什么时候用?——程序员写完代码,测试的时候。
4. VS调试快捷键
4.1 环境准备
首先是环境的准备,需要⼀个支持调试的开发环境。如果我们使⽤的是VS编译器,应该把VS设置为debug版本。
![](https://img-blog.csdnimg.cn/1ba10b336e024d1f8357b59b9c217840.png)
4.2 调试快捷键
F9:创建断点和取消断点
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂定执行,接下来我们就可以使用F10、F11这些快捷键,观察代码的执行细节。
条件断点:满足这个条件,才触发断点。
F5:启动调试,经常⽤来直接跳到下⼀个断点处,⼀般是和F9配合使用。
![](https://img-blog.csdnimg.cn/a0a848cb962340798bbcc5427c7058be.gif)
当有多个断点,按F5进行调试,箭头到达的是逻辑上的下一个断点,而不是物理上的断点。比如上图,第一个断点for循环还没完成,所以按F5以后箭头依然停在第10行。
![](https://img-blog.csdnimg.cn/2d1cf86f0a344aea998f289593c1f11d.gif)
而在设置条件i==5后,断电就会停止,屏幕上只打印了5个元素。
F10:逐过程,通常⽤来处理⼀个过程,⼀个过程可以是⼀次函数调用,或者是⼀条语句。
![](https://img-blog.csdnimg.cn/a862dbc19180467693ea16a25d214584.gif)
F11:逐语句,就是每次都执行⼀条语句,但是这个快捷键可以使我们的执行逻辑进⼊函数内部。在函数调用的地方,想进⼊函数观察细节,必须使用F11,如果使⽤F10,直接完成函数调用。
![](https://img-blog.csdnimg.cn/fd61deeff85c4a86b6152b6f04f98f31.gif)
CTRL + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
![](https://img-blog.csdnimg.cn/45658665bd754075a3db99f5baeb746e.gif)
5. 监视和内存观察
在调试的过程中我们,如果要观察代码执行过程中,上下文环境中的变量的值,有哪些⽅法呢?
![](https://img-blog.csdnimg.cn/802e0d9e49b84179a5ceaab5db41b06e.png)
观察的方法很多,这里主要介绍两种方法:监视和内存。
5.1 监视
开始调试后,在菜单栏中【调试】→【窗口】→【监视】,打开任意⼀个监视窗口,输⼊想要观察的对象就行。
在监视窗口观察对象:
![](https://img-blog.csdnimg.cn/c14812c0729b4f09a6bfe44f08334cb4.png)
5.2 内存
监视窗口看的不够仔细的话,也可以观察变量在内存中的存储情况,同样的操作,开始调试后,在菜单栏中【调试】→【窗口】→【内存】,打开任意⼀个内存窗口。
![](https://img-blog.csdnimg.cn/e327eb45d67a46b38c0b71480d2d392c.png)
在内存窗口观察对象:
在打开的内存窗口中,在地址栏输⼊arr,&i,&sz,这类地址,就能观察到该地址处的数据。
除了上述监视和内存以外,在调试的窗口中还有:局部变量、反汇编、寄存器等。
6. 两个调试的经典案例
6.1 调试案例1
求1!+2!+3!+4!+…10!的和。
首先我们先写一段代码来求一下 n!:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int i = 1;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
printf("%d\n", ret);
return 0;
}
运行结果:
![](https://img-blog.csdnimg.cn/569eb0d87fd44eb69d3242f6237c6169.png)
如果n分别是1,2,3,4,5…10,求出每个数的阶乘,再求和就好了。将代码更改如下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int n = 0;
int i = 1;
int sum = 0;
int ret = 1;
for (n = 1; n <= 3; n++)//如果n太大,不好判断代码是否正确,所以先用n=3来验证一下
for (i = 1; i <= n; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
运行结果:
![](https://img-blog.csdnimg.cn/99e52d546e9c4810a808914a95722279.png)
正常情况下,1!+2!+3!=9,而我们代码得出的结果是15,证明我们代码是有问题的,需要对代码进行调试,修复问题。
最开始n=1,i=1,进入循环里面,ret=1 * 1=1;循环以后,i++变成2,不满足i <= n,跳出内层循环,sum=1。
然后n=2,i=1,进入循环,ret=1 * 1=1;循环以后,i++变成2,满足i<=n,继续循环,ret=1 * 1 * 2;循环后,i++变成3,不满足i <= n,跳出内层循环,sum=1+2=3.
前两步还是按照我们我们所想一样进行,到了第三步就出问题了。
n=3,i=1,进入循环,ret=2 * 1;循环以后,i++变成2,满足i <= n,继续循环,ret=2 * 1 * 2;循环以后,i++变成3,满足i <= n,继续循环,ret=2 * 1 * 2 * 3;循环以后,i++变成4,不满足i <= n,跳出内循环,sum=1+2+12=15。
至此,我们已经找到问题的关键:n=3的时候,3!的阶乘应该为6,而ret的值却为12,说明开始循环的时候,ret的值不为1。而解决问题也很简单,只需要将ret的值在每次循环前初始化为1即可。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int n = 0;
int i = 1;
int sum = 0;
for (n = 1; n <= 3; n++)
{
int ret = 1;
for (i = 1; i <= n; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
运行结果:
![](https://img-blog.csdnimg.cn/eef10277825c4840869a2445351112a9.png)
而题目要求我们计算1!+2!+3!+…+10!,只需要将n=3改为拿n=10即可,运算结果如下所示:
![](https://img-blog.csdnimg.cn/e4c734b70d7c444f8a09dc04b61905cd.png)
6.2 调试举例2
在VS2013、X86、Debug 的环境下,编译器不做任何优化的话,下⾯代码执⾏的结果是啥?
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
运行结果:
![](https://img-blog.csdnimg.cn/ec384a597a1b496dada621f2304261d1.gif)
程序运行,死循环了。让我们调试看一下为什么。
![](https://img-blog.csdnimg.cn/329c2d86d1024f86bac023d660e038e3.gif)
从图中可以看出,当i增大到12时,arr[i]的地址和i的地址是一致的,arr[12]初始化为0,i也变为了0,就再一次进入到循环里边去。这也是程序死循环的原因。
可以用下面这幅图来对上面作进一步解释。
![](https://img-blog.csdnimg.cn/816b71709f594258a274df69d04be009.png)
1、由于整形变量 i 和数组arr 均为局部变量,因此两者都存放在内存的栈区中。
2、栈区的使用习惯是:先使用高地址的空间,再使用低地址的空间。因为在代码中,先创建i,再创建arr数组,所以i的地址一定是比arr更高的。如果i的地址创建在如图所示的位置,那么arr的地址一定它的下边。
3、又因为数组随着下标的增长,地址是由低到高变化的。那这时候,数组的下标一定是按图中所示变化的,只有这样才能满足“地址由低到高变化”的规律。
4、如果说arr和i之间有多余的空间,那么arr数组向后适当越界就有可能覆盖到i。至于这个多余的空间有多大,这是由编译器决定的。而当前这个代码在VS编译器上中间恰好空了两个整形,所以i<=12的时候就死循环了。
写到这儿,之前有一家公司的笔试题目和这道题差不多,感兴趣的小伙伴也可以尝试一下,我把题目放到下面。
![](https://img-blog.csdnimg.cn/7058737084ae491ca14de51ca69ac529.png)
9. 编程常见的错误
9.1 编译型错误
编译型错误⼀般都是语法错误,这类错误⼀般看错误列表就能找到⼀些蛛丝马迹,双击错误信息也能初步的跳转到代码错误的地方。随着对语法的熟练掌握,这种错误会越来越少。
![](https://img-blog.csdnimg.cn/e987275571f14d568aa9cf4c8090842a.png)
9.2 链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。这类问题一般为以下几种:
1、标识符名不存在
2、拼写错误
3、没包含头文件
4、引用的库不存在
![](https://img-blog.csdnimg.cn/f3f1edbe6f8348a89aedae35c8062475.png)
而且这类错误在错误列表中,前面会以LNK标识!
9.3 运行时错误
这类错误是千变万化的,也没有办法细说。需要我们借助调试,逐步定位问题。
好了,以上就是本期博客的全部内容了,喜欢的小伙伴不要忘了点赞和收藏,你们的点赞、收藏和关注是我不断前进的动力,我们下期再见!