【C语言学习】VS调试技巧

1. bug的概念

bug的本意是 “虫子” 和 “臭虫”,但是现在⼀般泛指电脑系统或程序中,隐藏着的⼀些未被发现的缺陷或问题,简称程序漏洞
bug一词的发明过程是非常有趣的,它最早来源于一位美国海军电脑专家——格蕾丝·赫柏(Grace Murray Hopper)。上世纪40年代,计算机还是由继电器和真空管驱动的,机器有房间那么大。体现当时技术水平的MarkII,是由哈佛大学制造的一个庞然大物。1947年9月9日,格蕾丝·赫柏对Harvard Mark II设置好17000个继电器进行编程后,对整机进行运行时,它突然停了工作。技术人员爬上去找原因,发现这台巨⼤的计算机内部⼀组继电器的触点之间有⼀只⻜蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。所以在报告中,赫柏⽤胶条贴上飞蛾,并用 “bug” 来表示 “⼀个在电脑程序里的错误”,“bug” 这个说法也⼀直沿用到了今天。

2. 调试的概念

当我们发现程序中存在的问题的时候,那下⼀步就是找到问题,并修复问题。而找到问题的过程就被称为调试,英文叫debug (解决问题的意思) 。
调试一般分为以下几个步骤:
1、发现程序错误的存在
2、以隔离、消除等但是去定位问题的位置
3、确定错误产生的原因
4、提出纠正错误的解决办法
5、对程序错误予以改正,重新测试
下面放一组漫画,让大家直观感受一下代码的调试过程。没错,这也是本人敲代码的状态😥。

3. debug和release

在VS编译器上编写代码的时候,总能看到有debug和release两个选项,分别是什么意思呢?
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序;程序员在写代码的时候,需要经常性的调试代码,就将这里设置为debug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运⾏速度上都是最优的,以便用户很好地使用。当程序员写完代码,测试再对程序进行测试,直到程序员的质量符合交付给用户使用的标准,这个时候就会设置为release,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息等。

debug版本

release版本

通过上述两个图片,我们可以直观的看出,两个不同版本的可执行文件的大小差异很大,debug版本要比release版本大得多

由于各自的特性,两种版本都有相应的应用场景:
Debug版本什么时候用?——程序员写代码的时候。
Release版本什么时候用?——程序员写完代码,测试的时候。

4. VS调试快捷键

4.1 环境准备

首先是环境的准备,需要⼀个支持调试的开发环境。如果我们使⽤的是VS编译器,应该把VS设置为debug版本。

4.2 调试快捷键

F9创建断点和取消断点
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂定执行,接下来我们就可以使用F10、F11这些快捷键,观察代码的执行细节。
条件断点:满足这个条件,才触发断点。
F5启动调试,经常⽤来直接跳到下⼀个断点处,⼀般是和F9配合使用。

有多个断点,按F5进行调试,箭头到达的是逻辑上的下一个断点,而不是物理上的断点。比如上图,第一个断点for循环还没完成,所以按F5以后箭头依然停在第10行。

而在设置条件i==5后,断电就会停止,屏幕上只打印了5个元素。
F10逐过程,通常⽤来处理⼀个过程,⼀个过程可以是⼀次函数调用,或者是⼀条语句。

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

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

5. 监视和内存观察

在调试的过程中我们,如果要观察代码执行过程中,上下文环境中的变量的值,有哪些⽅法呢?

观察的方法很多,这里主要介绍两种方法:监视和内存

5.1 监视

开始调试后,在菜单栏中【调试】→【窗口】→【监视】,打开任意⼀个监视窗口,输⼊想要观察的对象就行。

在监视窗口观察对象:

5.2 内存

监视窗口看的不够仔细的话,也可以观察变量在内存中的存储情况,同样的操作,开始调试后,在菜单栏中【调试】→【窗口】→【内存】,打开任意⼀个内存窗口。

在内存窗口观察对象:

在打开的内存窗口中,在地址栏输⼊arr,&i,&sz,这类地址,就能观察到该地址处的数据。
在这里插入图片描述
除了上述监视和内存以外,在调试的窗口中还有:局部变量、反汇编、寄存器等。

6. 两个调试的经典案例

6.1 调试案例1

求1!+2!+3!+4!+…10!的和。
首先我们先写一段代码来求一下 n!:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int i = 1;
	int ret = 1;
	for (i = 1; i <= n; i++)
	{
		ret = ret * i;
	}
	printf("%d\n", ret);
	return 0;
}

运行结果:

如果n分别是1,2,3,4,5…10,求出每个数的阶乘,再求和就好了。将代码更改如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int n = 0;
	int i = 1;
	int sum = 0;
	int ret = 1;
	for (n = 1; n <= 3; n++)//如果n太大,不好判断代码是否正确,所以先用n=3来验证一下
		for (i = 1; i <= n; i++)
		{
			ret *= i;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}

运行结果:

正常情况下,1!+2!+3!=9,而我们代码得出的结果是15,证明我们代码是有问题的,需要对代码进行调试,修复问题。
在这里插入图片描述
最开始n=1,i=1,进入循环里面,ret=1 * 1=1;循环以后,i++变成2,不满足i <= n,跳出内层循环,sum=1。
然后n=2,i=1,进入循环,ret=1 * 1=1;循环以后,i++变成2,满足i<=n,继续循环,ret=1 * 1 * 2;循环后,i++变成3,不满足i <= n,跳出内层循环,sum=1+2=3.
前两步还是按照我们我们所想一样进行,到了第三步就出问题了。
n=3,i=1,进入循环,ret=2 * 1;循环以后,i++变成2,满足i <= n,继续循环,ret=2 * 1 * 2;循环以后,i++变成3,满足i <= n,继续循环,ret=2 * 1 * 2 * 3;循环以后,i++变成4,不满足i <= n,跳出内循环,sum=1+2+12=15。
至此,我们已经找到问题的关键:n=3的时候,3!的阶乘应该为6,而ret的值却为12,说明开始循环的时候,ret的值不为1。而解决问题也很简单,只需要将ret的值在每次循环前初始化为1即可。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int n = 0;
	int i = 1;
	int sum = 0;
	for (n = 1; n <= 3; n++)
	{
		int ret = 1;
		for (i = 1; i <= n; i++)
		{
			ret *= i;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}

运行结果:

而题目要求我们计算1!+2!+3!+…+10!,只需要将n=3改为拿n=10即可,运算结果如下所示:

6.2 调试举例2

VS2013、X86、Debug 的环境下,编译器不做任何优化的话,下⾯代码执⾏的结果是啥?

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i <= 12; i++)
	{
		arr[i] = 0;
		printf("hehe\n");
	}
	return 0;
}

运行结果:

程序运行,死循环了。让我们调试看一下为什么。

从图中可以看出,当i增大到12时,arr[i]的地址和i的地址是一致的,arr[12]初始化为0,i也变为了0,就再一次进入到循环里边去。这也是程序死循环的原因。
可以用下面这幅图来对上面作进一步解释。

1、由于整形变量 i 和数组arr 均为局部变量,因此两者都存放在内存的栈区中。

2、栈区的使用习惯是:先使用高地址的空间,再使用低地址的空间。因为在代码中,先创建i,再创建arr数组,所以i的地址一定是比arr更高的。如果i的地址创建在如图所示的位置,那么arr的地址一定它的下边。

3、又因为数组随着下标的增长,地址是由低到高变化的。那这时候,数组的下标一定是按图中所示变化的,只有这样才能满足“地址由低到高变化”的规律。

4、如果说arr和i之间有多余的空间,那么arr数组向后适当越界就有可能覆盖到i。至于这个多余的空间有多大,这是由编译器决定的。而当前这个代码在VS编译器中间恰好两个整形,所以i<=12的时候就死循环了。

写到这儿,之前有一家公司的笔试题目和这道题差不多,感兴趣的小伙伴也可以尝试一下,我把题目放到下面。

9. 编程常见的错误

9.1 编译型错误

编译型错误⼀般都是语法错误,这类错误⼀般看错误列表就能找到⼀些蛛丝马迹,双击错误信息也能初步的跳转到代码错误的地方。随着对语法的熟练掌握,这种错误会越来越少。

9.2 链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。这类问题一般为以下几种:
1、标识符名不存在
2、拼写错误
3、没包含头文件
4、引用的库不存在

而且这类错误在错误列表中,前面会以LNK标识

9.3 运行时错误

这类错误是千变万化的,也没有办法细说。需要我们借助调试,逐步定位问题。
好了,以上就是本期博客的全部内容了,喜欢的小伙伴不要忘了点赞和收藏,你们的点赞、收藏和关注是我不断前进的动力,我们下期再见!
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值