文章目录
一、什么是bug?
🍎bug本意是 “昆虫” 或 “虫子”,现在一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题,简称程序漏洞。
“Bug”的创始人格蕾丝·赫柏(Grace Murray Hopper),她是一位从事美国海军工作的电脑专家,1947年9月9日,格蕾丝·赫柏对Harvard Markll设置好17000个继电器进行编程后,技术人员正在进行整机运行时,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。所以在报告中,赫柏用胶条贴上飞蛾,并用“bug”来表示“一个在电脑程序里的错误”,“Bug”这个说法一直沿用至今。
二、什么是调试?
🍋当我们发现程序中存在问题的时候,那下一步就是找到问题,并修复问题。
🍋这个找问题的过程被称为调试,英文叫debug(消灭bug的意思)。
🍋调试一个程序,首先得承认出现了问题,然后通过各种手段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的方式,找到问题所在的位置,然后确定错误产生的原因,再修复代码重新测试。
三、Debug和Release
在VS上编写代码的时候,就能看到有 debug 和 release 两个选项,分别是什么意思呢?
Debug通常称为调试版本式,它包含调试信息,并且不作任何优化,便于程序员调试程序;程序员在写代码的时候,需要经常性的调试代码,就将这里设置为 debug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。当程序员写完代码之后,再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就会设置为 release ,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息。
如果在两个版本下分别调试上面图片中的代码:就会得到Debug和Release两个版本下的可执行程序:(运行程序后,在下面的输出窗口会告诉我们生成的可执行程序(.exe)的文件位置)
可以看到同样的程序在两个版本下生成的可执行程序的大小是有差距的,相比较于Debug版本的可执行程序,Release版本的更小。
四、VS调试快捷键
那程序员怎么调试代码呢?
4.1环境准备
首先是环境的准备,需要一个支持调试的开发环境,我使用的编译器是VS2022,应该把VS上的环境设置为Debug,如图:
4.2调试快捷键
调试最常使用的几个快捷键:
🍅F9:创建断点和取消断点。
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂定执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节。
条件断点:满足这个条件,才触发断点。
🍅F5:启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用。
🍅F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
🍅F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数的内部。在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,会直接完成函数调用。
🍅Ctrl+F5:开始执行不调试,如果你想让程序直接运行起来而不是调试,就可以直接使用Ctrl+F5。
VS更多快捷键了解:http://blog.csdn.net/mrlisky/article/details/72622009
(1) F9设置的断点如下图,光标在第13行的语句处按一下F9就会出现一个红色的点,就是断点:(再按一下F9取消断点)
这个时候,再按F5调试起来后,程序就会直接来到断点处,在监视窗口里可以看到,在断点前面的程序其实已经执行了,然后程序就停在断点处。
如果一个程序中设置有多个断点呢?那按F5会执行到哪呢?
可以看到如果程序中有多个断点时,那第一次按F5会先执行到最前面的断点处,此时在监视窗口可以看到变量i的值是0,如果在第一次按F5的基础上,再按一次F5,变量i的值变为:
在第二次按F5的基础上,再按第三次F5,i的值变为:
再在监视窗口查看arr数组:
通过上面的测试我们可以看出,在设置断点后,再配合使用F5,那程序停顿的断点处其实是一个逻辑断点,意思是第一次执行到断点处,比如上面第9行的断点。第一次按F5调试起来,程序会停在这一行,断点前面的程序其实已经运行了,然后停在断点所在的语句行,当第二次按F5调试后,因为是一个for循环,所以在执行了arr[i]=i这条语句后,会返回上到上面的循环调整部分,也就是i++部分,然后来到循环条件的判断部分,再然后就第二次遇到断点,又停在断点处等待下一次的F5调试。所以为什么称之为逻辑断点就是这个意思。(注意:当F5执行到断点所在的行时只是停在这一行,但这一行的语句是不运行的)
(2) 条件断点:就拿上面的程序来说明什么是条件断点:在第9行设置条件断点,如下图鼠标放在断点上,单击鼠标右键:
条件断点就是当满足某种条件时(才会触发这个断点),程序才会停在断点处。上面设置了当i==5时,程序才会停顿在第9行:
满足条件的断点,不是说程序只能执行到断点处而不能再调试啦。就像上面,如果第二次按F5调试,由于i在等于5后,for循环会使i往上递增,i==5的条件就失效了,所以第二次按F5程序就一次性运行完了。
(3) F10是逐过程的执行语句(也就是一条语句一条语句的执行),但是当遇到调用函数时,不会进入到函数的内部,而是直接调用完函数。F10也可以配合F9、F5一起使用,如果在程序中发现有一段代码出现了bug,那可以在该处设置断点,然后再按F10逐语句的进行调试找问题。
由上图,如果确定是第二个for循环部分出现了bug,那在13行设置断点,之后先按F5调试停在断点处,接下来就按F10一步一步的执行来找bug。这样做的好处是,如果一开始就按F10,一个语句一个语句的执行,由于第一个for循环的循环条件部分太长了,那只按F10来执行程序就太费时了。而设置了断点后,F5调试起来就可以直接来到第二个for循环处,并且第二个for循环的循环条件部分很短。所以这就是F9、F5、F10配合使用的好处,当程序的语句(逻辑上)很短时,用F10执行就很好。
(4)F11与F10的区别就是:在遇到函数调用的时候,按F11能够进到函数内部,执行函数内部的每条语句,这样就能更细致地观察函数内部的代码是怎么运行的。所以比起F10,F11其实能更细致入微地执行程序中的语句。
比如我们定义了一个test函数,在main函数中调用test函数,我们先在调用test函数的地方设置一个断点,然后按F5调试起来后,来到test函数处,我们再按F11进入函数内部,进入函数内部后就可以按F10或者F11来执行语句都可以:
当然还有一个ctrl+F5快捷键,意思是开始执行但不调试。ctrl+F5和F5是有区别的:如果按ctrl+F5执行程序,那不管程序中有没有设置断点,程序都会一次性运行完。所以小伙伴们要熟练掌握这些调试快捷键,学会了这些调试快捷键就可以更好的调试程序,找程序中的bug。(注意:在笔记本上电脑上,如果单独按这些快捷键不起作用,那就按Fn+这些快捷键)
五、监视和内存观察
在调试的过程中,如果想观察代码执行过程中,上下文环境中的变量的值,有哪些方法呢? 这些观察的前提条件一定是开始调试后才能观察,比如按F10调试下面的代码:
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int num = 100;
char c = 'w';
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
}
return 0;
}
5.1监视
上面的代码按F10调试起来,就可以在菜单栏中的【调试】->【窗口】->【监视】,打开任意一个监视窗口,输入想要观察的对象即可:
监视窗口如下:(按F10调试完程序后的变量i和数组arr的值)
5.2内存
如果在监视窗口看上面代码中的变量的值看得不够仔细,也可以在内存中观察变量的存储情况。在【调试】->【窗口】->【内存】中,任意选择一个内存窗口:
在内存窗口观察数据:(边按F10边调试看内存中变量的值的存储情况)
在打开内存窗口后,要在地址栏输入:arr(数组名),&num,&c,这类地址,就能观察到该地址处的数据。如下图,当F10调试到i==9的时候的内存和监视:
除此之外,在调试的窗口中还有:自动窗口,局部变量,反汇编、寄存器等窗口,请自行验证使用。
六、调试举例
在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",原因是什么呢?
可以在调试中,从内存里面查看数组arr和变量i的存储情况:
当F10调试到for循环的部分,进入循环就会把数组arr中的元素全部改成0。但是你注意到数组的元素个数只有10个,而for循环已经越界使用内存中的空间啦,当for循环的循环变量i超过9增加到12时,从上面的内存中可以看到其实已经访问到变量i的空间了,当arr[12]=0执行完毕,其实就是将变量i的值改为了0。这样就让变量i就又回到了0,又从0开始往上遍历。所以造成了死循环。
那为什么会出现这种情况呢?为什么变量i在内存中的地址就刚好在数组arr的后面(高地址处)?解释一下:
🥥1.栈区内存的使用习惯是从高地址向低地址使用的,所以变量i的地址是较大的。arr数组的地址整体是小于i的地址的。
🥥2.数组在内存中的存放是:随着下标的增长,地址是由低到高变化的。
因为是先创建的变量i,然后才创建的arr数组,所以在栈区中变量i的地址相较于数组arr是高地址。所以是这种内存布局,那随着数组下标的增长,往后越界就有可能覆盖到i,这样就可能造成死循环。
至于为什么变量i和arr数组之间恰好会空出来2个整型的空间? 这里确实是巧合,在不同的编译器下可能中间空出的空间大小是不一样的,代码中这些变量的内存分配和地址分配是编译器指定的,所以不同的编译器之间就有差异了。这个题目是和环境相关的。
从这个例子中我们能够体会到调试的重要性,只有调试才能观察到程序内部执行的细节,就像医生给病人做B超、CT一样。
🥑注意:栈区的默认使用习惯是先使用高地址,再使用低地址的空间,但是这个具体还是要看编译器的实现。比如:在VS上切换到X64,内存空间的使用顺序就是相反的,在Release版本的程序中,这个使用的顺序也是相反的。
七、编程常见错误归类
7.1编译型错误
编译型错误一般都是语法错误,这类错误一般看错误信息就能找到一些蛛丝马迹,双击错误信息也能初步的跳转到代码错误的地方或者附近。编译型错误,随着语言的熟练掌握,会越来越少,也容易解决。
7.2链接型错误
看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是因为:
🍓标识符名不存在
🍓拼写错误
🍓头文件没包含
🍓引用的库不存在
7.3运行时错误
运行时错误,是千变万化的,需要借助调试,逐步定位问题,调试解决的是运行时问题。