VS2022调试技巧

1、什么是bug?

bug本意是指昆虫、虫子;现在是指程序隐藏的问题,程序漏洞;

Bug起源于一名美国海军电脑专家,她配置好17000个继电器之后开始编程,期间呢发生了故障,过去发现是一只飞蛾,飞蛾受光和热的影响就扑进去,造成了故障,她就把这个飞蛾贴在日记里面,表示这是一个程序里的bug。

2、什么是调试?

我们写代码的过程中,会存在问题,那我们就需要找到这个问题,并解决。

那找问题的过程就是调试debug (消灭bug)的意识;

调试代码的时候我们发现了问题,首先我们要承认出现了问题,找到问题的所在,再通过逐过程或者逐语句来观察细节,再去修复代码。

3、Debug和Release

首先我们在vs2022上面可以看到有两个版本。

Debug通常称为调试版本。不做任何的优化,里面包含调试信息,方便程序员对代码进行调试。当我们对代码进行编译的时候就会在这个代码的文件夹里的X64里面生成一个Debug文件

Release通常称为发布版本,进行了各种优化,不包含调试信息,当程序员Debug调试完后,发现没有问题,就改为Release版本 ,给用户使用。

当我们点进去Debug和Release文件,发现有.exe可执行程序,两个文件的大小是不一样的,Debug文件是Release文件的6倍,这是因为Debug是包含调试信息在里面的,所以Debug文件里的.exe就大一点。

 

 4、vs调试快捷键

4.1调试环境

调试是在Debug环境里面进行调试的。把vs上面的环境改为Debug。

4.2调试快捷键

F9:创建断点和取消断点;

断点:就是把这里断开,当你在一行代码前设置断点,那当程序执行到断点的时候就会断开,在这里停下来,然后我们在用F10,F11观察代码的细节;

条件断点:当这行代码满足设置的条件,就会触发这个断点;

F5:启动调试,跳到下一个断点处,通常是和F9搭配一起使用的;

F10:逐过程,处理一个过程,一个过程可以是指一条语句或者是一个函数;

F11:逐语句,当处理到一个函数是,如果想观察函数里面的细节,那就按下F11跳到函数里面,那其实F11和F10其实是一样的,只不过F11它的观察更细,F10遇到函数就当作一个过程处理,不会观察里面的细节。

CTRL+F5:开始执行不调试,直接运行程序;

下面代码,假设我们认为程序出现了问题,经过排查,问题是在下面,这时候我们可以按F9设置断点,代码运行到这里就会停止,这时候我们再去那按F10、F11。

假设有两个断点,当按下F5时,它会停在第一个断点处,当再按下F5时,它还是在第一个断点处,不会跳到第二个断点处,是因为跳到按下F5跳到第一个断点i=0;第二次按下F5跳到第一个断点i=1;是一个for循环,循环还没有结束,是不会跳到第二个断点的,

按下F5,是跳到理论逻辑上的断点,不是物理逻辑上的断点;

如果想跳到第二个断点,要么循环结束,要么再按F9取消断点。 

比如在这里设置一个断点,假设再第5次循环的时候出现了问题,这时候你可能按5次,那如果是5万次,第4万次出现了问题呢?这就不能一个一个按了,这时候我们可以设置一个条件断点。

右击断点,选择条件,设置为i==5;表示满足i==5的时候触发断点。

 

F10可以观察每个过程的;

每执行一条语句,按一次F10;那就会一条一条程序往下走,直到结束;

 

但是我们却不能观察到add函数里面的细节;如果想观察函数,那我们就按F11;

 5、监视和内存

5.1监视

我们再调试的时候,我们可以打开监视窗口,来观察程序里的每一个变量的值的情况;

我们打开vc2022上面的调试--窗口--监视。随便选一个1234都行。

举例代码

int main()
{
	int arr[10] = { 0 };
	int num = 100;
	char c = 'c';

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
	}
	return 0;
}

我们按照以上步骤按下F10调试,打开监视窗口, 按下F10,数组初始化为0;num=100;也可以看到字符c,可以看到循环里面的数值是怎么变换的,这些调试要自己动手去做做看,看文章是体会不到的,

 

5.2内存 

现在我们直到变量里的值是怎么变换的,我们再深入一下,我们也可以看看变量的地址。

调试--窗口--内存

 打开后,我们看数组arr,数组是整型,整型4个字节,那我们把列改一下,改为4;

 

我们打开内存,可以在上面输入地址,我们看数组的地址就直接输入数组名就可以,数组名就是数组的起始地址,看其他变量的地址要用取地址符号&;或者按F10逐个观察,观察后,最左边是变量的地址,中间是变量再内存中的值,我们内存中的值是以2进制存放的,但这里为了方便观察是16进制的,每一个cc表示一个字节;最右边对内存中的值尝试解读,对我们的参考意义不大; 

 

6、调试举例

求1 !+2!+3!+4!+...10! 的和

1! = 1*1

2!=1*2

3!=1*2*3

假设我们求N的阶乘,再把每个阶乘加起来,但我们假设就1~3的阶乘的时候,发现值是15,但是3!的阶乘是9啊,这时候我们就通过调试,查看代码发生了什么问题。

int main()
{
	int n = 0;
	//scanf("%d",&n);
	int ret = 1;
	int i = 0;
	int sum = 0;
	for (n = 1; n <= 3; n++)
	{
		for (i = 1; i <= n; i++)
		{
			ret *= i;
		}
		sum += ret;
	}
	printf("%d\n",sum);
	return 0;
}

通过调试怎么到算3!阶乘的时候是15呢?通过调试发现算3!的阶乘的时候,ret是2;但是我们算阶乘都是从1开始的,原来是2!的阶乘的时候,ret变为2了,没有初始化,这样我们通过调试就找到了程序的漏洞,我们就可以去解决这个问题,我们把ret初始化就可以了。 

int main()
{
	int n = 0;
	//scanf("%d",&n);
	int ret = 1;
	int i = 0;
	int sum = 0;
	for (n = 1; n <= 10; n++)
	{
		ret = 1;
		for (i = 1; i <= n; i++)
		{
			ret *= i;
		}
		sum += ret;
	}
	printf("%d\n",sum);
	return 0;
}

当然我们这个程序还可以进行一个优化

1!阶乘就是1*1

2!阶乘就是1*1*2

3!阶乘就是1*1*2*3

以此类推

int main()
{
	int n = 0;
	//scanf("%d",&n);
	int ret = 1;
	int i = 0;
	int sum = 0;
	for (n = 1; n <= 10; n++)
	{
		ret *= n;
		sum += ret;
	}
	printf("%d\n",sum);
	return 0;
}

 7、调试举例1

这个代码是在vs2022,Debug,x86的环境下

#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;
}

我们可以发现这个代码有一个明显的问题,就是数组越界了,当我们运行的时候代码死循环了;

我们调试代码发现 当i=12的时候,是把下标为12的元素改为0;怎么连i也改为0了呢?是不是i和arr[12]的地址是一样的?我们取它们的地址发现原来是一样的,那当程序把arr[12]改为0的时候,i也改为0了,所程序运行到i=12的时候,就会变成0,死循环了;

那为什么会这样呢?

注意变量i和数组arr是局部变量 ,存放在栈区,那栈区的空间我们放出来,假设是右图,那地址有高地址,有低地址,那栈区的默认使用习惯就是先使用高地址,再使用低地址,那 变量 i 就放在上面,数组放在下面,数组随着下标的增长,地址由低变高,所以数组在右图是这样放的,那变量 i 和数组arr之间空两个空间,是为了方便理解。

当程序循环下标的时候,把每个下标的元素改为0;顺着下标就把10、11、12也改为0,下标12和变量 i是同一块内存空间,那它们的值是一样的。这就是为什么这个代码是死循环的。

                                                                     

这个代码如果反过来,先定义arr数组,再定义i

 

那在栈区的内存空间就会反过来,i的内存空间就不会与arr数组重叠,但这是不行的,如果那样的话,那我们是不是每次都要这样定义呢?这样太麻烦了;

这个代码本质上就是数组越界了,数组没有越界是不会造成死循环的。 

 

注意这个代码是专门设计出来的,是依赖环境的,

如果是在vc6.0的话,i与arr之间是连续的,那循环i<=10,就会死循环

如果是在vc2022,i与arr之间是空两个整型的,那循环i<=12,就会死循环

如果是在gcc,那 i 与arr数组是空1个整型的, 那循环i<=11,就会死循环

 那当我们打印在不同的环境下打印 i 和arr的地址会有什么变化呢?

int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&i = %p\n",&i);
printf("&arr[9] = %p\n",&arr[9]);

在vc2022,Debug,x86,环境下,i的地址是大于数组的最后一个元素的地址的,那在栈区的地址是 i 在高地址上面,arr数组在 i 的后面

换个环境,在 vc2022,Debug,x64,arr[9]的地址大,i的地址小,反过来了,arr在上面,i在下面;

换个环境,在 vc2022,Release,x86,也是arr[9]的地址大,i的地址小,反过来了,arr在上面,i在下面;

 

 这些现象我们可以得知,虽然栈区的默认使用习惯是先使用高地址,再使用低地址,但是这个代码是依赖环境的,不同的环境是会变的;比如Release环境下,arr和i的地址就反过来了,i怎么都不会与arr地址重叠,就像我们之前所说的,Release版本是会对代码进行优化的,如果我们就这样看这个代码会发现是这个问题吗?不会,我们就需要调试来解决问题,这就是调试的重要性。

8、调试举例2

 比如我们之前写的扫雷游戏,调试放置雷的函数,我们再Setboard函数那一行打下断点,按F5跳到断点处,按F11跳到函数里面观察细节,按下F10或F11观察过程。

 再监视的窗口我们可以看到count的值,x、y的随机值,在上面哪行哪列放雷,然后count--;

注意在跳到函数里面观察数组里面的元素的值,是形参数组名,(逗号)N(想要观察的元素个数)

比如下面代码,调试的时候,想看函数里面的形参数组的值,发现看不到的; 

void text(int arr[])
{

}

int main()
{
	int arr[] = { 0,1,2,3,4,5,6,7,8,9,10 };
	text(arr);
	return 0;
}

 

这时候我们就要这么写才能观察到函数里面的形参数组的值;arr,10;

 

注意我们调试代码的时候要做到心中有数,这段代码是要干什么的;有上面功能;出现了上面问题,怎么调试等方面;

调试我们要运用起来,遇到运行错误的时候要多去运用调试去找到问题,解决问题,调试越多,你就越熟练,对代码的理解也就越深;

 9、编程的常见错误

我们写好.c文件转变为.exe可执行文件要经过编译和链接,编译的时候可能会发生编译错误,链接的时候可能会发生链接错误;

9.1编译型错误

编译型错误就是语法错误,写程序的时候对语法不熟悉,漏打了{ },[ ],;

比如下面代码漏打一个分号;下面就显示这是一个语法错误;你可以点击它;跳到错误那里,进行修改;

9.2链接型错误

 调用printf函数的时候忘记加头文件;写一个函数的时候没有定义或者定义函数的时候函数名写错了;

调用printf函数的时候没有加上头文件;就会报错链接型错误;下面显示的LNK就是链接错误的意思;这里我们只需要加上头文件就可以了;

下面代码就是函数定义的时候函数名写错了,下面也提示了add函数未定义,下面提示了add未定义,LNK就是链接型错误; 

 

9.3运行时错误

我们写完代码运行的时候会发生错误,就是运行时错误;这种错误千变万化,不像编译错误,链接错误,可以很快的解决,到后面熟悉了,这两种错误会更少;但是有时候的运行错误我们不能一眼就看出有什么问题;我们就需要借用到调试来找到问题,解决问题;关键我们在平时的写代码的过程中,多用调式,能够熟练的使用,这样对我们代码的理解会更深。

感谢观看!感谢指正! 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值