实用调试技巧
一、什么是bug
bug的本意是臭虫,后被引用为计算机程序的错误。
第一次被发现的导致计算机出现错误的飞蛾,也是公认的第一个计算机程序错误。从此之后,英语单词bug被IT界引申为计算机程序存在的漏洞。
二、什么是调试?有何意义
一名优秀的程序员就像是一名出色的侦探,每一次调试程序都是侦探尝试破案的过程。程序员一定是在不断地调试代码中进步的。
2.1 Debug的定义
调试(debug)是计算机行话,又称除错,是减少计算机程序或电子仪器设备中程序错误的一个过程。
bug整体上又可以分为两类:(1)语法错误:不遵循C语言的语法规则的错误。比如说
printf("abc\n") // 该语句末尾没有分好,就是犯了C语言的语法错误
(2)语义错误:顾名思义就是意思上的错误,可以简单地理解为遵循了C语言规则但是运行结果不正确的错误。比如说:
#include <stdio.h>
int main()
{
int a = 2;
int a2 = a * a;
int a3 = a2 * a2;
printf("a2 = %d,a3 = %d\n", a2, a3);// 一个数的平方和立方
return 0;
}
该程序输出如下:
a2 = 4,a3 = 16
但是整数2的立方为8,显然该程序的错误类型属于语义错误。
2.2 调试步骤
调试的基本步骤:
-
发现程序错误,只有三类人可以发现错误:程序员,测试人员,用户
-
以隔离、消除等方式对错误进行定位
-
确定纠正错误的原因
-
提出纠正错误的解决方法
-
对程序错误予以改正,重新调试
+++
2.3 Debug版本和release版本的介绍
Debug通常称为调试版本,它包含了调试信息,并且不作任何优化,便于程序员调试程序
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和执行效率上作出了最大优化,以便用户很好地使用产品。注意:(1)、测试人员是在release版本进行测试的;(2)、release版本下调试时,若遇到输入输出库函数则程序会进入到进入stdio.h文件。举个例子,体会release版本的优点如下:
在VS编译器中,编写简易的C代码,具体如下:
#include <stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
Debug版本如下:
Release版本如下:
经过对比这两个版本,发现即使是在如此少量的源代码下,两个版本的大小相差几十KB。可见Release版本是在进行各种优化以便用户使用。
三、windows平台的调试介绍
3.1调试环境的准备
本文的调试和运行都是基于Visual studio2022环境的。在VS中点击 debug 选项,才可以对代码进行调试 。
3.2 学会一些基础的快捷键
一些基础的快捷键:
F5 开始调试并跳到第一个断点处
ctrl + F5 ---- 开始执行(不调试)
F9 ------- 设置断点或取消断点
F10--------逐过程
F11--------逐语句 :若遇到函数,则会进入函数内部。
在代码量较大的情况下需要程序跳转到某一行开始调试程序,此时需要F5与F9配合使用。操作步骤:按F9出现红点,然后移动红点使其放在需要调试的代码所在的行。紧接着按F5让程序直接执行该条语句之前的所有代码与此同时程序处于即将执行该语句的状态。再按下F5时跳到执行逻辑上的下一个断点或者结束调试显示程序的运行结果 。
若按F10或F5等功能键不起作用,则先按住功能辅助键Fn不放,然后再按F10,F2等功能键,就能产生F5等功能键的原始作用。还有一种方法:先按住Fn键再按Esc键。此时电脑已锁定功能。比如F2不再是静音键而是其原来的功能比如重名,此时直接按F5就可以运行代码。
小技巧:按下F9并将鼠标移动到需要调试的行后,把鼠标放在小红点上然后右击鼠标出现菜单,紧接着点击条件。然后在条件表达式这一栏输入条件表达式的内容并按下关闭。比如,我想按F5调试该程序时,想让程序从变量i为3开始调试,如下图:
3.3、调试的时候查看程序当前信息
按下F10,使程序进入调试状态。点击顶栏的调试然后再点击窗口,有如下几个窗口需要注意:
自动窗口:自动添加上下文的变量并自动删除
局部变量:程序在执行过程中,自动添加程序的局部变量并自动删除
监视窗口:灵活地监视程序的任一变量,对象的变化。推荐使用该窗口观察程序的变化。
内存:可以查看变量,指针,数组等标识符在计算机中的内存布局
内存窗口有四个,任选一个即可。
汇编:看到内容都是把C语言代码翻译出来的汇编代码。在不同的平台上汇编代码是有细微差异的,但整体逻辑保持不变。
寄存器:寄存器和你的C语言程序所运行的平台有关,存放的是频繁使用的变量和数据。
调用堆栈:反映函数的调用逻辑。用处:当项目比较大时函数比较多不利于查看函数之间的调用,此时就可以查看调用堆栈这个窗口,观察函数之间的调用。
必备知识:栈作为一种数据结构,具有先进后出的特点。即最后调用的函数,首先被内存释放掉;第一个被调用的函数,最后被释放。
小贴士:在调试时,有些信息不好观察,这时需要用到一些小技巧。
例子一:
在VS中输入以下代码
#include <stdio.h>
void test(int* arr)
{
printf("%d", *arr);
return;
}
int main()
{
int arr[] = {1,2,3,4,5,6,7};
test(arr);
return 0;
}
程序运行如下:
当进入到test函数时,不能观察到arr数组的所有元素,只能观察到数组首元素。因为当主函数传递数组首元素地址到test函数时,在test函数内没有创建数组,test函数只能通过数组首元素地址访问到数组首元素。此时如果想在test函数里,查看数组的信息,那就只能看到数组首元素。这里有个小技巧:按F10调试,然后打开监视窗口,双击arr,然后输入逗号与要查看的数组元素的个数。如下图:
四、调试实例
该调试在VS编译器中的x86环境下运行的,具体代码如下:
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {0};
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("test%\n");
}
return 0;
}
该源代码在VS的debug版本运行,运行结果是陷入死循环,结果如下:
但是该源代码在release版本下可以正常运行,结果如下:
要解释上述变化的原因,我们需要一些预备知识。
(1)局部变量i和arr存储在栈内的。而在使用栈区的内存时,总是从高地址处开始使用,到低地址处结束。
(2)数组元素的地址总是随着数组元素的下标递增而递增。
(3)变量i的地址与数组元素arr[9]的地址相差多少个整型的大小取决于具体的编译器。在VS中,相差2个整型的大小。在gcc编译器中,相差一个整型的大小。在VC6.0中,变量i和数组元素arr[9]相差两个整型的大小即两者相邻。
具体解释:当循环执行到i为9时,依然可以继续访问arr[10],arr[11],arr[12]。但是数组arr与局部变量i相差多少个整型的大小取决于具体的编译器以及是x86环境还是x64环境。在VS编译器的x86环境下中,两个不同的局部变量相差2个整型的大小。此时数组元素arr[12]与局部变量的地址就是相同的。你也许会问数组越界访问不会报错吗?是的,数组的确在进行越界访问,但是程序陷入死循环,没有机会报错。
我们通过图片直观理解,看下图:
有了上述知识后,可以得出在源代码中变量i的地址总是与arr[12]的地址相同。我们可以通过调试观察两者的地址,如下图:
总的来说,它们产生截然不同的原因是代码优化导致的。变量在内存中开辟的顺序发生变化,影响到了程序执行的结果。
+++
写在最后:本人编程小白一枚,上述讲解如有错误,请各位斧正!
者相邻。如若不懂,可以看下图: