学会VS调试技巧,学习工作无烦恼

90a9ba60701a48279f9e488bae18bea1.jpeg

先赞后看,不足指正!

这将对我有很大的帮助!

所属专栏: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,展开调试技巧的介绍。


1. 什么是bug?

        bug本意是“昆虫”或“虫子”,现在一般是指在电脑系统或程序中,隐藏着的一些未被发现的缺陷或问题,简称程序漏洞。(具体介绍:第一个程序臭虫(Bug)的由来


2. 什么是调试(debug)?

        在我的理解看来,debug的全称就是Destroy bug(消灭臭虫)。

        当我们运行程序并发现程序中存在的问题的时候,那么下一步就是找到问题,并修复问题。这个时候我们就需要调试(debug),简单来说就是发现问题,解决问题的过程。


3. Debug 与 Release

380aceed7a9d49fd86d6572b02760e9c.png        我们在VS编译器中可以看到 DebugRelease 这两个选项,它们分别称为 调试版本 发布版本 ,具体区别如下表所示:

选项优点缺点
Debug包含调试信息,便于调试程序不作任何优化,且文件较大
Release进行了优化,代码大小和运行速度都是最优的,且文件较小不包含调试信息,不能进行调试

          图例对比:Debug文件与Release文件

be0e9d9e43184b85b2067f04281de27e.png

759d100b9d1d4d18ab4edc76312d099f.png

        从上面的图片中我们可以知道,编译生成的可执行文件(.exe)的大小,release版本明显较小,而debug版本明显较大。


4. VS调试快捷键

快捷键说明
F9插入断点和取消断点
F5启动调试,经常用来直接跳到下一个断点处,一般是和F9配合使用
F10逐过程,通常用来处理⼀个过程,一个过程可以是一次函数调用,或者是一条语句
F11逐语句,就是每次都执行一条语句,这个快捷键可以让我们的执行逻辑进入函数内部。在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,直接完成函数调用
Ctrl+F5开始执行不调试,此时程序直接运行起来而不调试就能直接使用

 注:调试程序需在debug版本环境下进行

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

2. 条件断点:满足这个条件,才触发断点。

3. 调试更多:了解更多VS快捷键 


5. 监视和内存观察 

5.1 监视

        在调试的过程中,我们如果要观察程序运行过程中前后变量值的变化,此时我们就要用到监视

比如观察下面的示例代码:

#include<stdio.h>

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逐语句调试) 

df19c55c36c24a42b938ef329cf49bad.png

 步骤二:监视变量、地址变化(通过对变量监视,可以更好地发现问题)

03b212c97ce0453285e35a21085f9d62.png

5.2 内存

        除了通过监视窗口观察变量值的变化,还可以通过内存窗口观察变量在内存中是如何存储的。

        还是以上面的代码为例:

步骤一:打开内存(先按下F11逐语句调试)

ffd872d6c99b4650a2f46596a71a8f67.png

步骤二:在内存窗口中观察数据

9d3437688b534cc9b98779584126242c.png890129cc3a1741f7ba1e99fb6d8a3675.png

c769af4160cb453b970634f57d0b10d2.png

        除此之外,在调试的窗口中还有:自动窗口,局部变量,反汇编、寄存器等窗口。


6. 调试举例

6.1 举例一

求1!+2!+3!+4!的值,运行下列代码:

#include<stdio.h>

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。 

调试分析过程及监视变量 

        第一次运算:未出现问题,继续调试

ddca5d6e1be448d7b4775ea018f1f80a.png

        第二次计算:未出现问题,继续调试

93c9eeceffd44784b7967f54732debaa.png

        第三次计算:出现问题

7554cdfd43da4d1bb565eb6a69a70b4f.png

        第四次计算:出现问题

bd7b83c384244a49b0e09b9858fc2660.png

        由上可知,为什么没有得到我们预期想得到的结果?原因是在每次计算时,使用ret后未更新ret的值,以至于出现与预期结果不符的情况。 

修改后的代码:

#include<stdio.h>

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;
}

6.2 举例二 

        在VS2022、X86、Debug 的环境下,编译器不做任何优化的话,下面代码执行的结果是啥?

#include <stdio.h>

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环境下运行)

        为什么会出现死循环的结果?代码运行起来不应该是数组越界访问吗?此时,我们还是要借助调试来发现问题。

        在数组未越界时,并没有出现问题

799b76fb1f6c4c2d93c4f80458bdd581.png

        而在数组越界时,我们继续调试代码可以观察到,arr[12]与i的值同步变化,此时代码运行出现死循环的情况

2ca189a7f5224aa5acdd3c517051174d.png

        为什么arr[12]与i的值会同步变化呢?我们可以猜想arr[12]与i在内存存储中的地址是相同的

37af49cec208477cbc33a80856e1ded1.png

        通过上图,我们可以验证猜想是正确的

至于为什么会出现这样的情况,我们要知道:

  1. 局部变量一般是存放在内存的栈区里的;
  2. 数组在内存中的存储:随下标的增大,地址由低到高变化;
  3. 栈区内存的使用习惯是从高地址向低地址使用的,所以变量i的地址是较大的。arr数组的地址整体是整体小于i的地址。 

图示

a40a37b231fc4fd6a33d7d264d32f431.png

        原因:在该环境下,i和arr 数组之间恰好空出来2个整型的空间,当越界访问到arr[12]时刚好与i的地址重合,此时arr[12]与i的值同步变化,代码出现死循环。

注意:

  1. 在不同的编译器下可能中间的空出的空间大小是不一样的,代码中这些变量内存的分配和地址分配是编译器指定的,所以的不同的编译器之间就有差异了。所以这个代码是和环境密切相关的。
  2. 栈区内存的使用习惯是从高地址向低地址使用的,具体要根据编译器实现。比如:在VS上切换到X64环境,这个使用的顺序就是相反的,在Release版本的程序中,这个使用的循序也是相反的。

6.3 举例三

        在上面我们学习了如何去简单调试,那该怎么去断点调试呢?

下列代码示例:

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开始调试。

94c31f8a40b141c09fd16b14fff8d314.png

        这样,可以直接完成第一步,得到运行结果:

ce855b785926499fb513d1b96b341c00.png

         还有,如果我们认为第一步的中间过程有问题,也可以在第一步插入断点,点击鼠标右键设置断点条件。

f1e07d2429c14e22ba74bc039c36e661.png
c637888f2a09437899041606c6217ba1.png

7. 编程常见错误归类

7.1 编译型错误

        编译型错误一般都是语法错误,这类错误一般看错误信息就能找到一些蛛丝马迹的,双击错误信息也能初步的跳转到代码错误的地方或者附近。编译错误,随着语言的熟练掌握,会越来越少,也容易解决。

85dde4f86a1f42cc88e3db15515c59fb.png

7.2 链接型错误

        链接型错误一般是由于:

  1. 标识符名不存在
  2. 拼写错误
  3. 头文件未包含 
  4. 引用的库不存在

b8e74ddc437049ad8e1cafbd98c15632.png

7.3 运行时错误

        运行时的错误是复杂多样的,一般需要借助调试,逐步定位问题,调试解决的是运行时问题。


8. 结语

        希望这篇文章对大家有所帮助,如果你有任何问题和建议,欢迎在评论区留言,这将对我有很大的帮助。

        完结!咻~

  • 44
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值