1.bug是什么?
bug相信大家都是耳熟能详的,从小时候大家打游戏的时候,有些人可能会说,我靠,这游戏有bug啊!那么bug到底是什么意思呢?
其实呢,bug本意为昆虫或虫子,不过现在通常是指某个程序出现了漏洞,或者是隐藏的缺陷问题,简称,程序漏洞。
2.什么是调试?(debug)
调试指的是当我们在写完某一段代码,完成某一个负责的部分后,我们肯定要对程序进行调试,好发现程序中出现的问题,那么接下来就是解决问题并消除问题。这个找问题的过程就叫做调试,英文名成为debug。
那么首先当我们的代码出现了问题时,去调试一个程序,我们要承认我们的代码出现了某个问题,然后通过调试去定位问题,去解决问题,而不是否认问题的出现。
3.Debug和Release
我们在vs上可以看到Debug和Release,那么他们分别代表着什么意思呢?
Debug被称为调试版本,它包含着调试信息,当我们写代码时,都是在Debug版本下敲写代码的,因为它可以直接进行调试。
Release版本被称为发布版本,简而言之,这是面向用户的版本,在Release版本下,我们的代码将不可以在进行调试,不过Release版本下,会对我们的代码进行一定的优化,使得程序在大小和速度上都是最优的,以便用户使用。当我们写完代码时,测试我们的代码程序达到用户的要求后,便会将Release版本交给用户使用。
如下图例子:
这是Debug版本的,让我们看看Release版本:
通过对比可以发现,Release版本下的代码确实会优化,比Debug版本下的大小要小。
4.VS调试快捷键
4.1配置环境
我们需要在Debug环境下测试代码
4.2常用快捷键
F9:创建断点和取消断点
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂停执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节。
F5:启动调试,经常用来直接跳到下⼀个断点处,⼀般是和F9配合使用。
F10:逐过程,通常同来处理⼀个过程,⼀个过程可以是⼀次函数调用,或者是⼀条语句。
F11:逐语句,就是每次都执行⼀条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部。在函
数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,直接完成函数调用。
CTRL+F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
5.监视和内存观察
5.1监视
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
char c = 'w';
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
}
return 0;
}
如上代码,如果我们不清楚这个程序是如何运行的,不知道数组里面的值如何去发生变化,那么我们可以通过监视去查看这个程序运行时的变化(开始调试后,在菜单栏中【调试】->【窗口】->【监视】,打开任意⼀个监视窗口,输⼊想要观察的对像),如下:
我们可以发现,当我们的程序还未运行时,里面的值都还未知,当我们按下f11后,我们可以逐语句的去观察程序所发生的变化:
我们可以观察到,现在我们的初始值都已经初始化好了,接下来,我们可以继续进一步观察变化:
我们可以发现,随着i的增加,数组arr中的内容也在随之发生变化,由此,我们可以很好的观察到整个程序的运行。
5.2内存
如果认为监视观察的不够仔细,我们还可以去通过内存去观察(跟监视同等操作,在这里不再叙说)
我们在这里可以观察到内存,右边的列可以换成1行2行等等:
右侧为内存中的数据解析,我们可以输入对应的地址去观察程序,
只需要&arr的地址即可,其他的变量也是相同的道理。
6.调试举例
6.1举例1
求1!+2!+3!+4!+…10! 的和。
我们首先可以想办法求出n的阶乘,再然后求和就好啦!
那么我们可以通过循环去完成这个操作,如下代码:
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int ret = 1;
for (int i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d\n",ret);
return 0;
}
可以发现5!确实是120,我们的代码并没有出错,如若出错,我们还可以通过监视与内存观察我们出错的位置,接下来,我们就要完成求和的代码了。
#include <stdio.h>
int main()
{
int sum = 0;
int n = 0;
scanf("%d", &n);
int ret = 1;
for (n = 1;n<=3;n++)
{
for (int i = 1; i <= n; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n",sum);
return 0;
}
假如我们求3!,如上,当n==1,2,3时,代表着求1!2!3!,再用sum求和,看似很完美,实则当我们运行起来时会发现:
不对啊?求和加一起为1+2+6不应该等于9吗,怎么会是15呢?那我们哪里出现了问题呢?我们来一起监视一下!
可以发现,当n等于1时,没问题,接下来继续看:
n等于2时,sum等于3,也没问题,继续看:
我们现在会发现,哎呀,这不对啊,这sum怎么就变成15了呢?我们再仔细一瞅,我靠,这ret怎么干成12了,这啥情况,继续往前看,ret变成了4,再继续看ret变成了2,到这里,真相已经大白了,这一切都是ret搞的鬼!
我们想要算阶乘,ret必须复原为1,但是我们发现ret在算阶乘时并没有复原为1,这就是问题的关键,所以我们做出如下改变,就能正确算出答案:
如下图:
#include <stdio.h>
int main()
{
int sum = 0;
int n = 0;
scanf("%d", &n);
for (n = 1;n<=10;n++)
{
int ret = 1;
for (int i = 1; i <= n; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n",sum);
return 0;
}
6.2举例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;
}
答案是,程序会死循环,那么为什么程序会死循环呢?我们可以通过监视来观察一下。
接下来进入for循环,进一步观察:
数组arr在慢慢每个元素变为0,相对应的在屏幕上打印hehe,我们看看当i超过11时会发生什么?
可以发现,arr【10】和arr【11】居然也都变为0了,那么我们来看看arr【12】会不会变成0呢?
可以发现,arr【12】居然和i一起变为了0?那我们来取一下它两的地址看看。
我们可以发现,arr【12】居然和i是同一个地址
栈区内存的使用习惯是从高地址向低地址使用的,所以变量i的地址是较大的,arr数组的地址整体是小于i的地址。 数组在内存中的存放是:随着下标
的增长,地址是由低到高变化的。
如果是左边的内存布局,那随着数组下标的增长,往后越界就有可能覆盖到i,这样就可能造成死循环的。
这⾥肯定有同学有疑问:为什么i和arr数组之间恰好空出来2个整型的空间
呢?这里确实是巧合,在不同的编译器下可能中间的空出的空间大小是不一样的,代码中这些变量内存的分配和地址分配是编译器指定的,所以的不同的编译器之间就有差异了。所以这个题目是和环境相关的。
注意:栈区的默认的使用习惯是先使用高地址,再使用低地址的空间,但是这个具体还是要编译器的实现。
比如:
在VS上切换到X64,这个使用的顺序就是相反的,在Release版本的程序中,这个使用的顺序也是相反的。
感谢大家的收看,如果认为这篇文章对你有所帮助,可以给一个点赞哦~~