C语言——VS2019实用调试技巧

前言

要想成为一个合格的程序员,不仅仅要会写代码,更要会调试代码。咔咔一通敲代码,敲出了BUG,这时就分两种程序员,一种是质疑编译器的程序员,“什么?我写出了BUG,是不是机器出了问题?”,然后接着就是迷信式地调试,直至解决BUG。另一种是默默地高效地调试代码,把BUG给修复了。一个合格的程序员不但要会写BUG,还要懂得通过调试来修复BUG,毕竟第一个人想写BUG那是拦都拦不住的。本篇文章案例所使用环境为VS2019

1.什么是BUG?

bug在英文中的意思是虫子,这又为什么会和计算机程序沾边呢?原来在1947年9月9日,技术员赫柏对继电器编程完毕后,尝试运行,发现继电器无法正常工作。在排查问题后,发现是一只飞蛾飞到了继电器里,触电身亡后,它的尸体导致了机器卡住。
因此在提交错误报告的时候,赫柏直接将飞蛾贴在了报告了,并标了“bug”,从而导致bug有了后面的衍生意。这也是计算机程序的第一个bug。
在这里插入图片描述

BUG通常指的是计算机程序程序因为编写错误从而产生的错误以及漏洞。

2.调试的重要性

2.1.什么是调试?

调试(英语:Debugging / Debug),又称排错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

2.2.调试的基本步骤

1、发现程序错误的存在
2、以隔离、消除等方式对错误进行定位
3、确定错误产生的原因
4、提出纠正错误的解决办法
5、对程序错误予以改正,重新测试

程序编写时的常见错误

通常程序的错误分为以下三种:

1、编译型错误
2、链接型错误
3、运行时错误

1、编译型错误:通常是指语法语法出现了问题而产生错误。
在这里插入图片描述

2、链接型错误:通常是指标识符名不存在或者拼写错误。
在这里插入图片描述

此时编译这段代码,编译器并没有报错。control + F5让代码运行起来。
在这里插入图片描述
当代码较多时,不方便一眼看出拼写错误,可以通过control+F 文本查找功能,输入错误的标识符来定位。
在这里插入图片描述

3、运行时错误:当编译和运行代码都没有问题时,程序此时运行发生了错误。这也是最难处里的错误。此时就需要借助调试,逐步定位问题。例如:

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

在这里插入图片描述
在这里插入图片描述
这个BUG我会在后面进行调试修复的演示。

2.3.debug版本与release版本的介绍

debug版本:通常被称为调试版本。它包含了调试信息,不对程序进行优化,便于程序员调试程序。
release版本:通常被称为发布版本。它不包含调试信息,所以无法进行调试。release版本对程序进行了各种的优化,使得程序的代码大小以及运行速度都是最优的,便于用户的使用。

//本段代码在debug版本与release版本的的代码大小区别

#include <stdio.h>
int main()
{
char str[]=“hello world!”;
printf(“%s\n”,str);
return 0;
}
debug版本与release版本的exe.文件的大小对比
在这里插入图片描述
在这里插入图片描述

//本段代码在debug版版本与release版本的区别
#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;
}

debug版本下程序死循环打印hehe。
在这里插入图片描述
release版本下程序并没有死循环打印hehe
在这里插入图片描述
这也是说明了程序在release版本下会进行优化。至于为什么在debug版本下会死循环打印hehe呢?首先可以肯定的错误是数组越界访问了,至于为什么会导致程序死循环呢?待会我会对此进行分析。

以上就是release版本与debug版本的一些区别。程序在release版本为什么不能调试?这里我就不做演示,下面将介绍VS2019环境下如何调试。如果你对此感兴趣,不妨在看完下文的介绍后上手试试看看,毕竟实践出真知。

3.VS环境调试介绍

补充:本文使用环境为VS2019。VS环境下,调试的方式和调试的逻辑大同小异。

3.1.环境准备

在当前环境下使用debug模式,这样才能正常的进行调试。
在这里插入图片描述

3.2如何开始调试?

开始调试步骤如下:
在这里插入图片描述
开始调试后,打开窗口菜单后,如图所示
在这里插入图片描述

未开始调试,打开窗口菜单。如图所示
在这里插入图片描述
所以进入调试是需要按快捷键或者在调试菜单中单击开始调试(s)进入调试。

3.3学会使用快捷键进行调试

合理的使用快捷键可以大大地提高我们的调试效率,所以学会使用快捷键进行调试是很重要的。
常用的快捷键:
F5

启动调试,经常用来直接跳到下一个断点处。

F9

创建或取消断点。

补充断点是在特定点暂停程序执行的特殊标记,使用断点可以检查当前程序状态和行为。断点一旦设置便保留在你的项目中,直到你明确删除它。

F10

逐过程,通常用来处理一个过程。一个过程可以使一条语句,或是一次函数的调用

F11

逐语句,就是每次都执行一条语句,但是这个快捷键可以使执行逻辑进入函数内部(这是最
常用的)。

CTRL + F5

开始运行不调试,直接让程序运行起来。如果不想进行调试可以直接使用此快捷键运行程序。

查看更多快捷键

3.4.查看当前程序的信息

3.4.1.查看当前临时程序变量的值

在这里插入图片描述
在调试时,观察变量的值的变化。
在这里插入图片描述

3.4.2.查看内存信息

在这里插入图片描述

在这里插入图片描述
通过&取地址操作符,找到变量存放的位置以及存储在内存中的值。
在这里插入图片描述

3.4.3.查看调用堆栈

在这里插入图片描述
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。
在这里插入图片描述

3.4.4.查看汇编信息

在调试开始之后,有两种方式转到汇编:
方式一:右击鼠标,选择【转到反汇编】:
在这里插入图片描述
方法二:在调试菜单中的窗口菜单中:
在这里插入图片描述
在这里插入图片描述

3.4.5.查看寄存器信息

在这里插入图片描述
查看当前的寄存器信息
在这里插入图片描述

4.调试问题代码

4.1.例一


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

为什么统计的各个阶乘的和是错的呢?
我们通过调试来看看
在这里插入图片描述
在这里插入图片描述
通过调试我们发现,当i = 3时,此时ret的值为2,所以才导致求3的阶乘时程序产生了bug。找到了问题,下面是分析该问题,这样每次统计完一个阶乘并把它放入累加的变量中,进入下一次循环前,要把ret的值修改为1,以确保求出阶乘是 正确的。故修改后代码如下

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;
		ret = 1;//每次求n的阶乘将ret的值改成1
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}

在这里插入图片描述

4.2.调试实例例二

//为什么死循环打印hehe?

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

数组越界访问后,这里程序为什么死循环打印hehe呢?调试一下你就知道。
在这里插入图片描述
通过监视发现,arr[12]的值随着 i 的值发生变化。
在这里插入图片描述
根据查看内存中的地址,可以发现,arr[12]的地址和i的地址是一样的。所以当i = 12进入循环,执行arr[i] = 0;后,i 的值又变成0。循环往复造成程序死循环输出hehe。需要注意的是:这段代码仅在VS2019 X86环境有效。下面我通过画图的方式来演示一下这段代码在内存中的情况。
在这里插入图片描述

越界访问会造成程序的错误,我们应该在使用数组的时候检查下标避免越界访问。

5.如何写出易于调试的代码

5.1.什么是优秀的代码?

  1. 代码运行正常
  2. bug很少
  3. 效率高
  4. 可读性高
  5. 可维护性高
  6. 注释清晰
  7. 文档齐全

编写优秀代码技巧

  1. 使用assert
  2. 尽量使用const
  3. 养成良好的编码风格
  4. 添加必要的注释
  5. 避免编码的陷阱。

5.2.优秀编码实例

//模拟实现库函数strlen
size_t my_strlen(const char* str)
{
	//断言判断指针有效性
	assert(str != NULL);
	//记录起始位置
	char* start = str;
	while(*str++)//str指向的内容不为\0指针++
	{
		;
	}
	//通过指针减指针求出字符长度
	return str - start - 1;
}

补充:

1、使用断言assert需要包含对应头文件 #include<assert.h>.
2、表达式应注意操作符的优先级问题。
3、通过const来修饰形式参数,保证指针的安全性
4、确定合适的返回类型。因为字符串长度不可能是负数,所以返回类型定义为size_t
5、添加合适的注释

5.3.const修饰指针变量

//const放在*号左边
int main()
{
	int num = 10;
	int n = 1000;
	const int* p = &num;
	*p = 20;//err
	p = &n;//ok
	return 0;
}

当const放在 * 号左边时,修饰的是指针变量指向的内容,指针变量指向的内容不可以被修改,但是指针变量本身可以被修改。

//const放在 *号右边
int main()
{
	int num = 10;
	int n = 1000;
	int* const p = &num;
	*p = 20;//ok
	p = &n;//err
	return 0;
}

当const放在 * 号右边时,修饰的是指针变量本身,指针变量不可以被修改,但是指针变量指向的内容可以被修改。

总结

只有多多动手调试,才能够有进步!当遇到bug时,不要再迷信式的调试。我们要尝试通过调试来验证我们的想法是否能够实现。也只有加化自身的调试技巧,才能够让编程之路越走越远。

  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玩铁的sinZz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值