实用调试技巧(上)
什么是bug
bug的原意是臭虫,虫子。
世界上首次出现bug,是在1947年9月9日,赫柏对Harvard Mark II设置好17000个继电器进行编程后,技术人员正在进行整机运行时,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。
所以在报告中,赫柏用胶条贴上飞蛾,并把“bug”来表示“一个在电脑程序里的错误”,“Bug”这个说法一直沿用到今天。
现在,bug一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题(如:软件运行中因为程序本身有错误而造成的功能不正常、体验不佳、死机、数据丢失、非正常中断等现象。),简称程序漏洞,是程序设计中的术语。
什么是调试
调试的定义
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
- 调试,形象的来说就是寻找蛛丝马迹,逆流而上,找到真相。
- 一名优秀的程序员都是一名优秀的侦探,每一次调试都是一次破案的过程。
- 调试就是寻找代码里面的问题并解决的过程。
发现程序的bug的有三类人:程序员自己,测试人员和用户
调试的步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
debug和release
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
- debug模式下可以调试和观察
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
- release(测试人员测试完后没什么问题)模式下不能够很好的观察
下图为debug和release模式在vs中的位置(以下均已vs2019为例)
二者的异同:
- debug文件夹下是调试版本的可执行程序,release文件夹下是发布版本的可执行程序(如下)
- 二者都需要在各自的模式下调试,才能产生exe文件
- debug模式下,可执行程序41kb(较大)
- release模式下,可执行程序9kb(较小)
Windows下的调试环境
调试环境的准备
windows环境下的开发环境可以为vs,如vs2019,vs2022,等 称为IDE,集成开发环境,它包含编辑器,编译器和调试器
linux环境下编译器为gcc,调试器为gdb;
将模式改为debug模式
快捷键
常用的快捷键有F5,Ctrl+F5,F9,F10,F11,如下
两个需要知道的前提:
- 对于热键被锁的情况下,他的快捷键可能需要加上fn键,比如说逐语句F11,在单独按F11没有反应的情况下,可以使用fn+F11
- 断点:打断程序执行,程序执行将会停在这里
F9设置断点:光标停在程序想要执行的那行处,按f9,出现如下的红点
按下f5调试后,执行就会停在断点处;
断点f9和调试f5是一起配合着使用的;
可以设置多个断点,每按一次f5,就会停在一个断点处;
在循环内部的断点,按循环的流程来看,相当于在循环内加了多个断点;
断点适用于代码数量较多条件下的调试或者是跨文件时使用;
f10逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
f11逐语句(会进入被调用的函数内部),每次都执行一条语句
ctrl+f5 不调试直接运行,即使加断点也不会停下来
调试过程中查看程序的相关信息
主要有自动窗口,局部变量,监视,内存,反汇编,寄存器,调用堆栈,断点窗口,如下:
自动窗口:动态变化着的,不一定好用,系统自动显示或删除信息,不一定满足自己的要求;
局部变量:只展示出局部变量,也是系统自动显示,不一定满足自己的要求;
监视:自定义,想看哪个变量,就可以看哪个变量,还可以删除。如下:
这个要监视的对象,不仅可以是变量,还可以是其他的,比如地址,如下:
如果是数组的话,需要 数组名,数组元素的个数,比如:arr,12 如下
内存 :在内存窗口可以看到地址,包括全局全量,局部变量,数组等,如下:
数组的地址直接写数组名,如下:
反汇编:能够看到翻译成汇编语言后是什么样子的(汇编代码),如下:
寄存器,寄存器也可以在监视窗口查看(直接写寄存器的名字),如下:
调用堆栈(栈:从顶上放进去,从顶上拿出来),如下:
断点窗口,可以看到所有的断点,即使不在这个.c文件下,也能看到。如下:
实例分析
示例一
求 1!+2!+3! …+ n!
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
int j = 0;
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
运行结果如下:
通过计算,我们发现上述的结果并不正确,于是我们进行调试(逐语句)
我们发现ret在循环的过程中,它的上一轮的值被保存了下来;而我们是不需要在上一轮的基础上计算的,每轮结束后,都需要将其置1。
或者这样观察,我们发现当n=2的时候,sum的结果是没有问题的,因此可以推断出n=3时,程序存在问题,我们可以通过设置断点的条件,来直接达到n=3时的调试(f5)开始时刻。如下:
此种方法特别适用于循环过程中的调试,可以大大的节省时间。