C语言学习中的实用调试技巧(11)


前言

本篇文章以VS2022环境下为背景进行调试技巧的教学

一、什么是bug?

  bug本意是“昆虫”或“虫子”,现在一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题,简称程序漏洞
  实际上,这个词背后还有一件趣事,1947年9月9日,美国海军工作的电脑专家Grace Murray Hopper,对继电器进行编程,整机运行时,它突然停止了工作,于是爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压杀死,Hopper把这只飞蛾贴在了报告上,于是,“一个电脑程序里的错误”用“bug”来表示便成了一个习惯,保留到了现在。
在这里插入图片描述

二、什么是调试(debug)?

  de前缀有“去除”的意思,这里就取此意,即“去除bug”,解决问题的意思
  调试一个程序,流程基本上都是先承认出现了问题,然后再通过各种手段定位到问题,可能是逐过程的调试,也可能是隔离和屏蔽代码的方式,找到问题所处的位置,然后确定错误产生的原因,再修复代码,重新测试

三、Debug和Release

在这里插入图片描述
  VS上有Debug和Release两个选项,分别是什么意思呢?
  Debug称为调试版本,它包含调试信息,并且不作任何的优化,便于程序员调试程序
  Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用,当程序员写完代码的时候,测试再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就设置成release,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含任何调试信息。
具体而言:一个62KB,一个11KB
在这里插入图片描述
在这里插入图片描述
Debug不做任何优化;Release模式下,编译器可能自作主张进行优化

四、VS调试快捷键

F9:创建断点和取消断点
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂定执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节

条件断点:满足这个条件,才触发断点
举个例子,我们在设置断点后,按F5启动调试,正常情况下,i等于0
可我们可以在一开始就在断点处按右键设置断点,再设置条件i == 8
在这里插入图片描述
在这里插入图片描述

F5:启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用

F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句

F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部,在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,则直接完成函数调用

CTRL+F5:开始执行不调试,如果想让程序直接运行起来而不调试就可以直接使用

更多VS快捷键可了解

五、监视和内存视察

监视

在调试的过程中,如果要观察代码执行过程中,上下文环境中的变量的值,有哪些方法呢?
这些观察的前提条件都是一定要先调试再观察,比如:
在这里插入图片描述
先断点再调试,F5是让程序运行到断点处停止,要有点可停止!
这时候,下方会有“就绪”一行
在这里插入图片描述
这时候要怎么打开监视窗口呢?
先调试,再窗口,最后随便选个监视窗口即可
在这里插入图片描述
这样就能开始监视了,观察想要查看的变量
在这里插入图片描述

内存

同样的,我们按F5启动调试后,调试->窗口->内存,接着随便任选一个,就能查看内存
这个技巧也很重要,数据在内存中的存储到最后都是以二进制的形式,只是展现形式不同,要学会看内存
在这里插入图片描述

现在,我们想看看arr在内存中的存储,这时候我们需要地址,这怎么办呢?
先别急,其实数组名字就是数组的地址,关于这点,我们先不纠结,记着就好

输入arr,按下回车键即可,会自己变成相应的地址
在这里插入图片描述
在这里插入图片描述

请注意,上图中的0准确来说是0x0,即十六进制的0,上图中两个0合在一起,表示一个字节,所以你会发现,上下两个地址之间差了4(字节),而我们的数组刚好是int类型,一个int类型的数据在内存中刚好是四个字节

综上所述,上图中从上到下依次代表arr[0],arr[1]…arr[10]
为了让你理解的更深刻,我们将arr[2]设置为100
在这里插入图片描述

发现了么,竟然真的是第三个数变成了100!(0x64即为十进制数的100,6 * 16 + 4 = 100),至于为什么是从数据低位在左而不是在右,也就是说为什么是64 00 00 00 而不是 00 00 00 64,我们留在后面再说~

另外,关于内存窗口,我们发现左边是内存地址,中间是内存中的数据的16进制显示,右边其实是内存中的数据解析,实际上理解成没用,应该是没问题的,至少我从不留意这个,感觉没啥影响。

六、调试实例

实例1:阶乘之和

我们现在写了一个求阶乘之和的程序,但是运行起来结果是15,与我们想要的9相悖
在这里插入图片描述

出错了!我们就开始分析
有两个for循环,外层for用来求n次,里层for用来求每个阶乘,明白这个逻辑后,我们开始调试
当调试到n等于3,且里层for走完之后,我们发现,这时候ret == 12!,不应该啊,3!应该是6才对
在这里插入图片描述

显然,我们发现了问题所在,在求第三个阶乘的时候,出现了问题

这时候我们再看内层的逻辑,是通过让ret从开始,与1相乘,并把结果存储到ret中,再与2相乘并以此类推,可是,这个逻辑必须确保ret的初始值是1,我们显然没有做到,这就是根源,解决方法是再外层for的内部第一行增添一句ret = 0;语句

至此,调试初显锋芒

实例2:

在VS2022,X86,Debug环境下,编译器不做任何优化的话,下面代码执行结果如何?
在这里插入图片描述

程序运行起来,我们会发现陷入死循环了,调试看看是为什么?

当程序运行到i == 9的时候,i继续加1,还会进入循环,此时我们想查看arr[10],发现是能改动的

也就是说,i不仅明目张胆的越了arr的界,甚至还明目张胆的改了arr[10]的数据!这是我们万万不能接受的

继续按F11,我们会发现,arr[11]的数据也被改了,但arr[12]呢,我们会发现arr[12]一开始就并不是随机数,而是跟i一样的12,并且当执行完arr[12] = 0;这一语句后,i也变成了0,并再次0~12循环,这是为什么呢?

其实,i与arr[12]同步,说明它们是同一内存空间,实际证明下也确实如此,它们内存地址相同
在这里插入图片描述

我们不妨来研究一下深层的原因,首先arr和i都是局部变量,存储在栈区,栈区中内存的使用一般是从高地址到低地址,而变量i的地址是较大的,arr数组的地址整体是小于i的地址,而数组在内存中的存放则是,随着下标的增长,地址是由低到高变化的
在这里插入图片描述

因此,我们不难理解当数组下标增长,是很有可能覆盖到i的,而这里为什么刚好差2个整型的空间呢?
哈哈,是巧合啦,这取决于编译器,不同编译器对内存的分配是不一定的,所以与环境相关

本题又是借助调试,让我们发现了这一问题,究了根,还探了源,实在不错

七、编译常见错误归类

编译型错误

 一般都是语法错误,最简单的错误,一般写的越多,就越不会错这个问题

链接型错误

 看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在,一般是因为:
  1.表示符号不存在
  2.拼写错误
  3.头文件没包含
  4.引用的库不存在

运行时错误

 这个就千变万化了,需要借助调试来一一分析。

以上总总,当我们深刻理解了什么是编译,什么是链接后,应该就明白了


总结

  调试是一个很重要的技巧,不知道大家直到程序其实是可以调试的时候,有没有一种想去试试的冲动
  它很重要,非得你自己去尝试不可,光看是绝对不能参透
  事在人为,你快去给你的程序做做“B超”吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值