文章目录
- 什么是bug?
- 什么是调试(debug)?
- debug和release
- VS调试快捷键
- 环境准备
- 调试快捷键
- 监视和内存观察
- 监视
- 内存
- 调试举例1
- 调试举例2
- 调试举例3:扫雷
- 编程常见错误归类
- 编译型错误
- 链接型错误
- 运行时错误
什么是bug?
bug本意是“昆虫”或“虫子”,现在一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题,简称程序漏洞。
“Bug” 的创始⼈格蕾丝·赫柏(Grace Murray Hopper),她是一位为美国海军工作的电脑专家,1947年9月9日,格蕾丝·赫柏对Harvard Mark II设置好17000个继电器进行编程后,技术⼈员正在进行整机运行时,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。所以在报告中,赫柏用胶条贴上飞蛾,并把“bug”来表示“⼀个在电脑程序里的错误”,“Bug”这个说法一直沿用到今天。
什么是调试(debug)?
当我们发现程序中存在问题的时候,那下一步就是找到问题,并修复问题。这个找问题的过程被称为调试,英文叫debug(消灭bug的意思)。
调试一个程序,首先是承认出现了问题,然后通过各种手段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的方式找到问题,然后确定错误产生的原因,再去修复代码,重新测试。
debug和release
在VS上编写代码的时候,就能看到有debug和release两个选项,分别是什么意思呢?
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
程序员在写代码的时候,需要经常性的调试代码,就将这里设置为debug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的。
当我们设置为debug,写一段代码,生成解决方案
这个时候编译出的文件的路径在Debug文件夹底下生成了一个test_9_15.exe可执行程序
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。当程序员写完代码,测试再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就会设置为release,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息等
什么叫做发布版本呢?
就是软件开发人员以及把这个程序开发好了,开发好后交给测试人员测试,测试人员经过一定的测试后发现没什么问题,这个版本就可以发布出去给用户使用了,给用户使用的版本就是发布版本(也叫release版本)。
我们设置成release然后用这段代码生成解决方案
生成了release版本的可执行程序
那么怎么看代码大小和运行速度都是最优的呢?
- 先右键打开所在的文件夹
- 再到上一层的路径上,点进x64
- 会发现我们刚刚编译生成的两个版本的程序:debug和release分别放在了不同的文件夹底下
- 当我们进入到debug文件下后,发现生成的可执行程序是61KB
再回到上一层路径下,进入release文件夹下,发现release版本的可执行程序只有11KB
由此看来,生成的release版本在代码大小上遥遥领先。
这是为什么呢?
因为debug版本里面得包含调试信息,为了能调试代码,记录额外的信息,同时它也不能任何的优化,所以使得它的大小比较大。而release版本是用户使用的版本,不用包含任何的调试信息,也有各种优化,所以代码大小和运行速度都是最优的。
VS调试快捷键
那程序员怎么调试代码呢?
环境准备
首先是环境准备,需要一个支持调试的开发环境,我们使用的是VS,应该把VS上设置为debug,如图:
调试快捷键
调试最常用的几个快捷键:
- F9:创建断点和取消断点(通常F9都是与其他快捷键配合使用)
断点的作用是可以在程序的任意位置打上断点,打上断点就可以使得程序执行到这个位置时停下来,暂停执行。接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节。
条件断点:满足这个条件,才触发断点。
eg:
当我在29行代码这里按F9打上断点
再按F5直接跳到这行代码上,箭头来到29行代码这里,此时 i 的值为0。这种断点是普通断点
当我们鼠标移动到断点处右击鼠标,就会出现“条件”俩字,单击条件
勾选条件
此时我们就可以设置条件了
例如我们在里面设置,当i==5的时候才触发这个断点。输入“ i==5 ”后回车,再点关闭。
当我们按F5直接调试时,会发现“ i==5 ”时就自己停下来了
这种就叫做条件断点,想要在什么条件停下来,就设置条件断点
- F5:通常与F9配合使用,先用F9在那一行代码上打上断点,再按F5即可跳转到那一行代码(前面跳过的代码都会执行)
注意:想运行程序的时候不能直接按F5,因为F5不是用来运行程序的,而是用来跟F9配合调试用的。因为代码中一个断点也没有设置,这时程序会直接把所有代码全部执行完毕,停在最后。并且在一些编译器里按F5是不会停下来的,执行完程序后一闪而过,什么也没有,看不到调试控制台那个窗口。
当调试过程中想要跳到后面的某一行代码,但还需要执行多行代码时,可直接使用F9在想要跳到的那一行代码上打上断点,再按F5,就可以直接跳到那一行代码处(跳到那一行代码时,前面的跳过的代码也照样执行完毕)
注意:
当我们在第11行按F9打上断点,再按F5跳转到这行代码上,此时这行代码前面的断点处就会出现一个箭头,并且前面略过的代码也打印出了10个“hehe”
要是我们再在13行代码前打上断点,那么下次按F5时,箭头会不会直接跳转到13行代码的断点上呢?
答案是:箭头没有跳转到13行
我们发现,再次按F5时箭头并没有跳转到13行代码前,而是仍然在第11行代码面前,并且多打印了“haha"
这是因为程序在执行过程中,因为循环的原因,程序会又一次来到11行这个断点处,看到断点后就停下来了。
所以F5的作用是让箭头来到执行逻辑上的下一个断点处,而不是物理上的下一个断点。
如果想箭头跳转到13行处,就在11行处按F9取消断点,然后再按F5就可以了,此时10个“haha”也打印了
- F10:逐过程,通常用来处理一个过程,一个过程可以是一条语句,或者是一次函数调用(直接执行完函数,不进入函数内部)。
- F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部。在函数调用的地方,想进入函数内部观察细节,必须使用F11,如果使用F10的话,会直接完成函数的调用。
- CTRL+F5:开始执行不调试。如果你想让程序直接运行起来不调试代码,就可以直接使用CTRL+F5。
VS更多快捷键了解:http://blog.csdn.net/mrlisky/article/details/72622009
监视和内存观察
在调试的过程中,我们如果要观察代码执行过程中变量的值,有哪些方法呢?
例如:
在这段代码中,我们想知道程序在执行的过程中i变成多少了,该怎么办呢?
这时候就可以打开监视窗口来观察
监视
那么该怎么打开监视的窗口呢?
注意:在打开监视窗口之前的前提是正在调试
添加要监视的项后就可以观察到变量的变化了,这个过程中变量的变化就可以被观察(监视)到了
并且如果还想看i的地址,则在监视窗口写上“取地址i”就可以看到i的地址
甚至还可以在监视里放上一个表达式
并且会根据程序的变化而变化
内存
看完了监视窗口,我们也可以看看内存窗口
我们运行以下代码,可以通过监视来查看数组的各种变化:
而我们有时候不仅仅要看到变量的变化,也要看到内存的变化,所以我们要打开内存窗口
内存中的数据里,一个字节占一个地址。
例如:“4c”占一个字节,“8d”占一个字节,“8c”占一个字节…
此时我们想看arr数组里面的内容,则可在地址栏中输入数组的地址。
那么怎么输入数组的地址呢?
数组名其实是数组首元素的地址(也就是数组名就是数组空间起始位置的地址),并且数组在内存中是连续存放的,所以直接在数组栏中输入arr就可以了。
因为这个数组是整型数组,每个元素是一个整型,一个整型是4个字节,在中间的内存数据中,一个地址是一个字节。所以我们可以把列设为:一行显示4列,即每行就是一个数组元素。
接着按F11调试,看内存随代码的变化而变化
调试举例1
求 1!+2!+3!+4!+…10! 的和,请看下面代码:
阶乘:
5的阶乘:5!==5*4*3*2*1
4的阶乘:4!==4*3*2*1
如果要求上面各个阶乘的和,我们可以先写个代码求出n的阶(n!),n为几,几的阶乘就出来了。然后再把求出的每个阶乘加起来就行了。
首先我们先写出个代码求出n的阶乘
求n的阶乘就是1~n的乘积,例如5的阶乘为:1~5的乘积,即1*2*3*4*5。
所以我们写个代码求出n的阶乘就行了
当输入的n为3的时候,i=1小于3满足条件进入第一次循环。
此时i=1,ret=1.
进入循环ret=1*1=1.(此时算的可以看作是1的阶乘)
循环完后此时i=1,ret=1.
i=2进入第二次循环,此时i=2,ret=1.
进入循环ret=1*1*2=2(此时可以看作是2的阶乘)
循环完后i=2,ret=2
i=3进入第二次循环,此时i=3,ret=2
进入循环ret=1*1*2*3=6(此时可以看作是3的阶乘)
因为i<=n,所以当i为几的时候,就是几的阶乘。
当n输入其他数字也同理,用ret=ret*i即可算出1到n的乘积
我们也可以用另一种方式
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{int n = 0;int i = 0;int ret = 1;int sum=0;for (n = 1; n <= 3; n++){for (i = 1; i <= n; i++){ret *= i;//ret=ret*i}sum+=ret;//sum=sum+ret}printf("%d\n",sum);return 0;
}
由上例的代码知道,内循环i为几就是求得几的阶乘
此段代码由n<=3可知,求得是1!+2!+3!的和
外循环n=1时进入内循环,内循环里面就是求1的阶乘
当内循环里i=2不满足i小于等于n(1)时,内循环停止进入外循环,执行表达式三,调整变量n=2,也就是再次进入内循环接着求2的阶乘…以此类推
但最后的结果确实错误的,因为1!+2!+3!应该等于9,而这里却等于15
所以,我们要调试找出问题在哪
调试就是照着程序走,看一看程序有没有按照我们预期的方式走。如果没有按照预期走,那么说明代码有问题
-
按F11,调出监视窗口,将我们创建的变量添加上去,以便观察。
接下来要进入循环了,看看代码是不是按照我们设想的走 -
按照我们的想法,n为1<=3进入循环,n为1,内循环求得就是1的阶乘。所以我们调试看看监视窗口是不是求得1的阶乘
n=1的整个循环结束都没有任何问题,我们再往下走,当n++为2再次进入循环验证剩下的步骤 -
n变为2了,内循环求得就是2的阶乘了。n为1时i为2,此时进入内循环又将被初始化为1
n为2的整个循环过程也没有问题,内循环ret=1*1*2=2,求得是2的阶乘,加上sum为3 -
n变为3,内循环求得就是3的阶乘
- 当i再次被初始化为1<=3程序继续往下走时
结果: - i++变为2继续执行程序
结果: - i++变为3继续执行程序结果:
所以
因为ret是在循环外部创建的,外面创建后里面使用不初始化的话,会逐渐产生累积效果。
我们要算某个数阶乘的话ret应该从1开始
如果上一次ret产生的值不是1,算的数字会越来越大。
例如:我们算了2!在后面算3的阶乘的时候是在ret=1*2的基础上再乘的3!。也就是ret=1*2*1*2*3 - 当i再次被初始化为1<=3程序继续往下走时
所以,在每一次算一个数的阶乘之前,都应该将ret初始化为1
正确代码:
上段代码虽然写完了,但是不是一段好代码,接下来优化一下代码
因为
1!==1*1
2!==1*1*2
3!==1*1*2*3
4!==1*1*2*3*4
我们发现:
只要知道3!,算4!就是3!*4;
只要知道2!,算3!就是2!*3
所以有没有那个必要像上段代码循环那样,求每个数字的阶乘都从1开始乘呢?
答案是没必要的,所以我们优化一下代码:
调试举例2
在VS2022、X86、Debug的环境下,编译器不做任何优化的话,下面代码执行的结果是啥?
#include <stdio.h>int main(){int i = 0;int arr[10] = {1,2,3,4,5,6,7,8,9,10};for(i=0; i<=12; i++){arr[i] = 0;printf("hehe\n");}return 0;
}
运行:
我们原本的猜想是会越界访问,但是事实上并没有报错,并且还在死循环的打印“hehe”
所以我们调试看看到底是为什么会变成死循环
- 按F11开始调试,打开监视创库,添加变量i与数组,以便观察
- 按照循环将数组下标为0~9的值全部改为0,并打印完10个“hehe”后,仔细观察arr[10]
- 当i==10时进入循环,在监视窗口添加arr[10],观察
此时arr[10]是个随机的值
那么我们将代码运行一下,看它的值会不会变化
可以看到居然变了,越界了。并且打印了第11个“hehe” - 接下里我们观察arr[11]继续调试,看看会不会改
- 继续观察arr[12]那么看会不会被改变
我们发现最后一次i的值变为了0,这就意味着此次循环后i执行i++会变为1,接着继续进入循环。
并且随着i的值变化,arr[12]的值也跟着变化。这是为什么呢?难道它们是同一块空间?
为了验证整个问题,我们取地址i(&i)取地址arr[12](&aee[12])看看它们的空间
我们发现,地址一模一样。这就说明我们改arr[12]的时候 i 必然会改。当 i 为12的时候,再给一次机会就为13,但是它们的地址相同,arr[12]的值被改为0,i的值也就跟着变为0,这样的话 i 永远也不会变为13,这个循环永远也不可能停下来。
通过调试我们发现了这个现象导致了我们程序的死循环了
调试举例3:扫雷
如果是一个代码稍微复杂,那怎么调试呢?
这里我们就上手调试一下扫雷的代码:
- 在函数内部打断点,快速跳转到函数
先用F9打断点,在按F5可快速跳转到那个地方 - 在数组传参,调试进入函数,如何在监视窗口观察数组的内容
一维数组:
但是当arr数组传给test函数时,只能看到一个元素,看不到整个数组的内容
此时添加监视项:arr,10.就可以看到全部了
二维数组:
当进入函数test2的时候
同理,如果想看到全部,则输入“arr2,3(3行的意思)”
编程常见错误归类
编译型错误
编译型错误一般都是语法错误,这类错误一般看错误信息就能找到一些蛛丝马迹,双击错误信息也能初步的跳转到代码错误的地方或附近。编译错误随着语言的熟练掌握会越来越少,也容易解决。
例如在语句后面少写了“分号”
链接型错误
看错误信息,主要找到错误信息中的标识符,然后定位问题所在。一般是因为
- 标识符名不存在
- 拼写错误
- 头文件没包含
- 引用的库不存在
运行时错误
运行时错误:指的是没有编译错误,没有连接错误,程序能够运行,但是结果是错误的。
这种错误是千变万化的,需要借助调试,逐步定位问题,调试解决。