C语言——VS实用调试技巧

什么是bug?

bug本意是“昆虫”或“虫子”,现在一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题,简称程序漏洞。

“Bug” 的创始⼈格蕾丝·赫柏(Grace Murray Hopper),她是一位为美国海军工作的电脑专家,1947年9月9日,格蕾丝·赫柏对Harvard Mark II设置好17000个继电器进行编程后,技术⼈员正在进行整机运行时,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。所以在报告中,赫柏用胶条贴上飞蛾,并把“bug”来表示“⼀个在电脑程序里的错误”,“Bug”这个说法一直沿用到今天。


什么是调试(debug)?

当我们发现程序中存在问题的时候,那下一步就是找到问题,并修复问题。这个找问题的过程被称为调试,英文叫debug(消灭bug的意思)。
调试一个程序,首先是承认出现了问题,然后通过各种手段去定位问题的位置,可能是逐过程的调试,也可能是隔离和屏蔽代码的方式找到问题,然后确定错误产生的原因,再去修复代码,重新测试。


debug和release

在VS上编写代码的时候,就能看到有debugrelease两个选项,分别是什么意思呢?
在这里插入图片描述
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.youkuaiyun.com/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

所以,在每一次算一个数的阶乘之前,都应该将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行的意思)”在这里插入图片描述

编程常见错误归类

编译型错误

编译型错误一般都是语法错误,这类错误一般看错误信息就能找到一些蛛丝马迹,双击错误信息也能初步的跳转到代码错误的地方或附近。编译错误随着语言的熟练掌握会越来越少,也容易解决。
例如在语句后面少写了“分号”
在这里插入图片描述


链接型错误

看错误信息,主要找到错误信息中的标识符,然后定位问题所在。一般是因为

  • 标识符名不存在
  • 拼写错误
  • 头文件没包含
  • 引用的库不存在

运行时错误

运行时错误:指的是没有编译错误,没有连接错误,程序能够运行,但是结果是错误的。
这种错误是千变万化的,需要借助调试,逐步定位问题,调试解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值