先赞后看,不足指正!
这将对我有很大的帮助!
所属专栏:C语言知识
阿哇旭的主页:Awas-Home page
目录
引言
环境准备
1. 什么是bug?
2. 什么是调试(debug)?
3. Debug 与 Release
4. VS调试快捷键
5. 监视和内存观察
5.1 监视
5.2 内存
6. 调试举例
6.1 举例一
6.2 举例二
6.3 举例三
7. 编程常见错误归类
7.1 编译型错误
7.2 链接型错误
7.3 运行时错误
8. 结语
俗话说的好:“工欲善其事,必先利其器”。而我们的C语言学习也是如此,不仅要打牢基础,也要使用工具来辅助我们的学习,好比“君子生非异也,善假于物也”。
那么,话不多说,我们一起来看看吧!
基于Visual Studio 2022,展开调试技巧的介绍。
bug本意是“昆虫”或“虫子”,现在一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题,简称程序漏洞。(具体介绍:第一个程序臭虫(Bug)的由来)
在我的理解看来,debug的全称就是Destroy bug(消灭臭虫)。
当我们运行程序并发现程序中存在的问题的时候,那么下一步就是找到问题,并修复问题。这个时候我们就需要调试(debug),简单来说就是发现问题,解决问题的过程。
我们在VS编译器中可以看到 Debug和Release 这两个选项,它们分别称为 调试版本 和发布版本 ,具体区别如下表所示:
选项 优点 缺点 Debug 包含调试信息,便于调试程序 不作任何优化,且文件较大 Release 进行了优化,代码大小和运行速度都是最优的,且文件较小 不包含调试信息,不能进行调试
图例对比:Debug文件与Release文件
从上面的图片中我们可以知道,编译生成的可执行文件(.exe)的大小,release版本明显较小,而debug版本明显较大。
快捷键 说明 F9 插入断点和取消断点 F5 启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用 F10 逐过程,通常用来处理⼀个过程,一个过程可以是一次函数调用,或者是一条语句 F11 逐语句,就是每次都执行一条语句,这个快捷键可以让我们的执行逻辑进入函数内部。在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,直接完成函数调用 Ctrl+F5 开始执行不调试,此时程序直接运行起来而不调试就能直接使用 注:调试程序需在debug版本环境下进行
1. 断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂定执行,接下来我们就可以使用F10,F11这些快捷键,观察代码的执行细节。
2. 条件断点:满足这个条件,才触发断点。
3. 调试更多:了解更多VS快捷键
在调试的过程中,我们如果要观察程序运行过程中前后变量值的变化,此时我们就要用到监视。
比如观察下面的示例代码:
#include
int main()
{
int i = 0;
int sum = 0;
for (i = 0; i < 14; i++)
{
printf("%d ", i);
sum += i;
}
printf("\n总和:%d\n", sum);
return 0;
}
步骤一:打开监视(先按下F11逐语句调试)
步骤二:监视变量、地址变化(通过对变量监视,可以更好地发现问题)
除了通过监视窗口观察变量值的变化,还可以通过内存窗口观察变量在内存中是如何存储的。
还是以上面的代码为例:
步骤一:打开内存(先按下F11逐语句调试)
步骤二:在内存窗口中观察数据
除此之外,在调试的窗口中还有:自动窗口,局部变量,反汇编、寄存器等窗口。
求1!+2!+3!+4!的值,运行下列代码:
#include
int main()
{
int i = 0;
int j = 1;
int sum = 0;
int ret = 1;
for (i = 1; i <= 4; i++)
{
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
运行结果:303
很显然,这不是我们想要得到的答案,但我们可以通过调试来找出问题
目标结果:33
运行顺序:1!=1,sum=1;2!=2,sum=3;3!=6,sum=9;4!=24,sum=33。
调试分析过程及监视变量
第一次运算:未出现问题,继续调试
第二次计算:未出现问题,继续调试
第三次计算:出现问题
第四次计算:出现问题
由上可知,为什么没有得到我们预期想得到的结果?原因是在每次计算时,使用ret后未更新ret的值,以至于出现与预期结果不符的情况。
修改后的代码:
#include
int main()
{
int i = 0;
int j = 0;
int sum = 0;
int ret = 1;
for (i = 1; i <= 4; i++)
{
ret = 1; // 每次阶乘完成,更新ret的值
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
在VS2022、X86、Debug 的环境下,编译器不做任何优化的话,下面代码执行的结果是啥?
#include
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("haha\n");
}
return 0;
}
运行结果:死循环的打印“haha”。(在X86环境下运行)
为什么会出现死循环的结果?代码运行起来不应该是数组越界访问吗?此时,我们还是要借助调试来发现问题。
在数组未越界时,并没有出现问题
而在数组越界时,我们继续调试代码可以观察到,arr[12]与i的值同步变化,此时代码运行出现死循环的情况
为什么arr[12]与i的值会同步变化呢?我们可以猜想arr[12]与i在内存存储中的地址是相同的
通过上图,我们可以验证猜想是正确的
至于为什么会出现这样的情况,我们要知道:
- 局部变量一般是存放在内存的栈区里的;
- 数组在内存中的存储:随下标的增大,地址由低到高变化;
- 栈区内存的使用习惯是从高地址向低地址使用的,所以变量i的地址是较大的。arr数组的地址整体是整体小于i的地址。
图示
原因:在该环境下,i和arr 数组之间恰好空出来2个整型的空间,当越界访问到arr[12]时刚好与i的地址重合,此时arr[12]与i的值同步变化,代码出现死循环。
注意:
- 在不同的编译器下可能中间的空出的空间大小是不一样的,代码中这些变量内存的分配和地址分配是编译器指定的,所以的不同的编译器之间就有差异了。所以这个代码是和环境密切相关的。
- 栈区内存的使用习惯是从高地址向低地址使用的,具体要根据编译器实现。比如:在VS上切换到X64环境,这个使用的顺序就是相反的,在Release版本的程序中,这个使用的循序也是相反的。
在上面我们学习了如何去简单调试,那该怎么去断点调试呢?
下列代码示例:
int main()
{
int i = 0;
int j = 0;
for (i = 0; i < 88; i++) // 第一步
{
printf("断点调试测试");
}
for (j = 0; j < 44; j++) // 第二步
{
printf("调试");
}
return 0;
}
通过观察,我们可以确认第一步没有什么问题,可能在第二步出现问题,如果慢慢调试,要调试88次才会到达第二步,这样效率就很低了。
此时,我们可以适当地设置断点。步骤:选中要调试的行数,按F9插入断点,然后按F5开始调试。
这样,可以直接完成第一步,得到运行结果:
还有,如果我们认为第一步的中间过程有问题,也可以在第一步插入断点,点击鼠标右键设置断点条件。
编译型错误一般都是语法错误,这类错误一般看错误信息就能找到一些蛛丝马迹的,双击错误信息也能初步的跳转到代码错误的地方或者附近。编译错误,随着语言的熟练掌握,会越来越少,也容易解决。
链接型错误一般是由于:
运行时的错误是复杂多样的,一般需要借助调试,逐步定位问题,调试解决的是运行时问题。
希望这篇文章对大家有所帮助,如果你有任何问题和建议,欢迎在评论区留言,这将对我有很大的帮助。
完结!咻~