bug本意是“昆⾍”或“⾍⼦”,现在⼀般是指在电脑系统或程序中,隐藏着的⼀些未被发现的缺陷或问题,简称程序漏洞。 “Bug” 的创始⼈格蕾丝·赫柏(Grace Murray Hopper),她是⼀位为美国海军⼯作的电脑专家,1947年9⽉9⽇,格蕾丝·赫柏对Harvard Mark II设置好17000个继电器进⾏编程后,技术⼈员正在进⾏整机运⾏时,它突然停⽌了⼯作。于是他们爬上去找原因,发现这台巨⼤的计算机内部⼀组继电器的触点之间有⼀只⻜蛾,这显然是由于⻜蛾受光和热的吸引,⻜到了触点上,然后被⾼电压击死。所以在报告中,赫柏⽤胶条贴上⻜蛾,并把“bug”来表⽰“⼀个在电脑程序⾥的错误”,“Bug”这个说法⼀直沿⽤到今天
当我们发现程序中存在的问题的时候,那下⼀步就是找到问题,并修复问题。 这个找问题的过程叫称为调试,英⽂叫debug(消灭bug)的意思。 调试⼀个程序,首先是承认出现了问题,不能说自己的代码一定没有错,否则是永远找不出bug的。然后通过各种⼿段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的⽅式,找到问题所的位置,然后确定错误产⽣的原因,再修复代码,重新测试。
在VS上编写代码的时候,就能看到有 debug 和 release 两个选项,分别是什么意思呢?
通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序;程序员在写代码的时候,需要经常性的调试代码,就将这⾥设置为 debug ,这样编译产⽣的是debug 版本的可执⾏程序,其中包含调试信息,是可以直接调试的,放在了代码文件夹下的x64中的Debug文件夹中
Release 称为发布版本,它往往是进⾏了各种优化,使得程序在代码⼤⼩和运⾏速度上都是最优的,以便⽤⼾很好地使⽤。当程序员写完代码,测试再对程序进⾏测试,直到程序的质量符合交付给⽤⼾使⽤的标准,这个时候就会设置为 release ,编译产⽣的就是 release 版本的可执⾏程序,这个版本是⽤⼾使⽤的,⽆需包含调试信息等,放在了代码文件夹下的x64中的Release文件夹中
两种不同版本的文件大小对比如下: Debug:
Release:
对⽐可以看到从同⼀段代码,编译⽣成的可执⾏⽂件的⼤⼩,release版本明显要⼩,⽽debug版本明显⼤
调试最常使⽤的⼏个快捷键: F9:创建断点和取消断点断点的作⽤是可以在程序的任意位置设置断点,打上断点就可以使得程序执⾏到想要的位置暂停执⾏,接下来我们就可以使⽤F10,F11这些快捷键,观察代码的执⾏细节。 条件断点:满⾜这个条件,才触发断点 F5:启动调试,经常⽤来直接跳到下⼀个断点处,⼀般是 和F9配合使⽤。 F10:逐过程,通常⽤来处理⼀个过程,⼀个过程可以是⼀次函数调⽤,或者是⼀条语句。 F11:逐语句,就是每次都执⾏⼀条语句,但是这个快捷键可以使我们的执⾏逻辑进⼊函数内部。在函数调⽤的地⽅,想进⼊函数观察细节,必须使⽤F11,如果使⽤F10,直接完成函数调⽤,不会进入函数内部。 CTRL + F5:开始执⾏不调试,如果你想让程序直接运⾏起来⽽不调试就可以直接使⽤。 VS更多快捷键了解:http://blog.csdn.net/mrlisky/article/details/72622009
以以下代码为例:
此代码没有做任何的输出操作,我们如果想要观察这些变量究竟发生变化没有,或者看看它们在内存中如何存储,就需要使用监视或者内存功能
首先要先进入调试模式,按下快捷键f10启用调试,或者按【调试】->【开始调试】按钮启用调试,随后按【调试】->【窗⼝】->【监视】,然后任意找一个窗口点开,如下:
点开后将打开的窗口放在右边,如图:
随后我们将要查看的变量输入进去,如:
这里数组arr只显示一个内容,点右边三角形展开,而后显示无法读取内存,就是还没有调试到那一步,按f10继续走下去,走到for循环前再来观察:
可以看到前面的内容已经初始化完成了,接下来的for循环就是给数组赋值,可以自行按f10调试,这里我们直接来看看for循环执行的结果:
可以看到,for循环成功为我们完成了任务,如果这个代码直接运行什么都不会出现,但是经过我们的调试,我们可以清楚的看到我们写的代码确实起了效果
如果监视窗⼝看的不够仔细,也是可以观察变量在内存中的存储情况,首先还是要进入调试状态,然后菜单中【调试】->【窗⼝】->【内存】打开内存窗⼝,如图:
现在我们把内存窗口打开试试
内存中这三大类分别是什么呢?如图:
我们怎样通过内存观察数据呢?就要用到我们的取地址符&,比如想要查看字符变量c,就在内存框中输入&c,如图:
要求 1!+2!+3!+4!+…10! 的和:
#include <stdio.h>
//写⼀个代码求n的阶乘
int main()
{
int n = 0;
scanf("%d", &n);
int i = 1;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d\n", ret);
return 0;
}
#include <stdio.h>
int main()
{
int i = 1;
int j = 1;
int ret = 1;
int sum = 0;
for (i = 1; i <= 10; i++)//求1到10的阶乘的和
{
for (j = 1; j <= i; j++)//求阶乘
{
ret *= j;//ret是阶乘的值
}
sum += ret;//sum是各个阶乘相加的结果
}
printf("%d\n", sum);
return 0;
}
是不是我们的代码完成了呢?运行结果是一个很大的数,我们也不知道对不对,这个时候我们将10改成3试试,运行结果如下:
我们简单计算一下,1的阶乘是1,2的阶乘是2,3的阶乘是6,相加是9,与代码算出来的15不一样,错在哪里呢?我们现在肉眼看不出来,就要启用我们的调试功能,按f10一步一步的调试,i=1时循环调试结果为:
可以发现现在一切正常,帮我们算出来了1的阶乘,并且加到了sum内部,j=2跳出了第一次for循环,接下来是第二次循环调试结果:
我们发现居然还是一切正常,代码帮我们算出来了2的阶乘,sum变成了3,j变成了3跳出循环,到这里一切正常,我们猜到多半下一次循环有问题,我们来看看下一次循环调试的结果:
果然,这里出现了问题,ret也就是3的阶乘变成了12,导致我们的阶乘和变成了15,这是为什么呢?我们可以反推一下,我们原本需要得到6,现在变成了12,说明多乘了一个2,这个2怎么来的呢?仔细一想我们就会发现,这个2是之前算出来的2的阶乘,出现这种多乘的结果就是因为每执行一次ret,ret的值就会被改变,由于1的阶乘就是1,ret还是1,所以不会影响下一次求2的阶乘,但是由于2的阶乘是2,ret变成了2,就影响了求3的阶乘,所以问题就出在ret身上,我们需要确保每一次求一个数的阶乘,ret都是1,所以解决办法就是在每一次循环开始前,就将ret重置为1,改正代码为:
#include <stdio.h>
int main()
{
int i = 1;
int j = 1;
int ret = 1;
int sum = 0;
for (i = 1; i <= 10; i++)
{
ret = 1;
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
注意:经过这次调试举例,我们可以看出,有一些情况下我们下意识认为我们的逻辑是正确的,被蒙在鼓里,找不到问题,这时我们就要一步一步调试查找错误,调试对我们的帮助非常大
这个举例对我们的编译环境有要求,需要在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 %d\n",i);
}
return 0;
}
一运行,我们发现代码居然死循环了,这是为什么呢? 我们来看看以上代码在内存中的存储:
我们可以看出三点:
根据这三个特点,我们不难想到,当i=12的时候,将arr[12]就与i重合了,此时将arr[12]改成了0,也就是将i改成了0,这时循环判断又从0开始了,到下一个12又出现这种情况,周而复始,导致了代码死循环 当然,这种代码只是特例,是要看环境的,在VS上切换到X64,栈区使⽤的顺序就是相反的,在Release版本的程序中,这个使⽤的顺序也是相反的,也就导致arr[12]和i不会重合,也就不会死循环,最多报一个越界访问的警告
注意:从这个例子看出来,有些代码会根据环境的不同而产生不同的结果,比如栈区的默认的使⽤习惯是先使⽤⾼地址,再使⽤低地址的空间,但是这个具体要看编译器的实现,但是我们牢记一点,只要我们规范写代码,基本上不会出问题,上面例子中也是数组越界访问导致死循环,只要规范写代码,不写出这种越界访问的错误,就会大大减小出错的概率