C语言:实用调试技巧(VS2019)


前言:

相信很多小伙伴在初学编程语言时,常常会被自己写出的代码,因各种问题而导致的bug而搞得头痛不已,但想查找错误的时候又是无从下手的的迷茫,今天这篇文章将彻底打消你的疑惑,帮助你快速且准确的锁定你代码中的错误信息点并修改。
同时在文章后半部分也会教大家如何避免bug的产生,以及高质量的写出易于运行和调试的代码。
接下来就让我们一起来看看吧。

什么是bug

这里先科普一个计算机小历史,bug的由来。

世界上第一个bug

1944年世界上第一台计算机马克1号诞生,在世界上第一位女程序员葛丽丝·霍普(Grace Hopper)接手下,顺利改造成马克二号。

1946年的一天,霍普敲代码的时候发现计算机发生了故障,就在马克二号的继电器触点里,找到了一只被夹扁的小飞蛾。
正是这只小虫子卡住了机器的运行。

霍普顺手将飞蛾夹在工作笔记里,而备注的意思是臭虫,正是这一奇怪的称呼,奠定了Bug这个词在计算机世界的地位,bug也变成无数苦逼工程师的噩梦。这就是第一个bug的诞生。

在这里插入图片描述

调试是什么?有多重要?

所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,
就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。
顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。

一名优秀的程序员是一名出色的侦探。
每一次调试都是尝试破案的过程。

我们是如何写代码的?

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

拒绝-迷信式调试!!!!

调试是什么?

调试(英语:Debugging / Debug),调试是保证所提供的设备能够正常运行的必须程序。又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

调试的基本步骤

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

debug和release的介绍

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。

Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

对于下列代码debug和release的区别:

#include <stdio.h>
int main()
{
	int a = 10;
	printf("%d\n", a);
	return 0;
}

在上述代码所属的项目下分别产生debug和release的应用程序
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
从上图中两种不同版本的相同代码所产生的应用程序的大小却相差30kb。


而且两种版本的反汇编代码也有差异:
debug:
在这里插入图片描述
release:
在这里插入图片描述

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版本下代码,程序陷入死循环。


在这里插入图片描述
release版本下代码,程序正常运行。

原因:

变量在内存中开辟的顺序发生了变化,影响到了程序执行的结果。


注意:

在release环境下调试不易观察到变量的变化,所以平时程序员所编写代码的环境就是debug环境下的。

所以我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。

windows环境调试介绍

调试环境的准备
在环境中选择 debug 选项,才能使代码正常调试。
在这里插入图片描述

学会快捷键

在上方任务栏中调试窗口中:
在这里插入图片描述

最常使用的几个快捷键:
F10

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

#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

当我们按F10进入调试界面后,再继续按F10,程序就会从main函数开始一行一行的向下执行语句。
在这里插入图片描述
按F10可以从main函数的第一条语句依次执行到main函数结束。
但如果main函数中出现了其他函数调用,这时,你又想进入这个函数内部观察程序运行过程,这时你按F10会发现,箭头一下就跳过函数,进入下一条语句了,这是因为F10是逐过程执行的,一次执行跳过一行,无法进入函数内部观察。
这时你就需要使用F11进行调试了。

F11

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

#include <stdio.h>
void test()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
}
int main()
{
	int a = 10;
	test();
	printf("%d ", a);
	return 0;
}

当我们按F10进入调试界面后,再继续按F10,程序运行到test函数时。
在这里插入图片描述

我们按一下F11。
在这里插入图片描述

箭头就会进入到test函数内部,再按F10就可以一行一行的执行代码了。


F5

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

F9

创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点。
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。

F5与F9一般是配合来使用的。
示例:

#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

当我们需要在程序执行到某条语句时停下来,就需要在那条语句前使用F9打断点,然后再按F5,程序就会执行到断点处停下来。

打断点操作:

先用鼠标选中要打断点的语句行,再按F9就打好断点了。再按一下F9就取消该行的断点了。
或者直接在该行打断点处用鼠标点一下,就打好断电了。再点一下也就取消该行的断点了。

打断点:
在这里插入图片描述
按F5:
在这里插入图片描述
程序执行到断点处停止。
接着再使用F10或F11继续调试即可。


注意:在调试时可以打多个断点,每按一次F5就会执行到下一个断点处。
在这里插入图片描述

当我们想进入循环体内部观察,假设我们要在 i == 5 的时候让程序停止下来,就可以在断点处设置一下停止条件。
在这里插入图片描述

我们用鼠标右键点击该断点,在此菜单中点击条件选项。
在这里插入图片描述
在条件设置框中输入断点条件即可。
在这里插入图片描述

在这里插入图片描述

此时按F5。
在这里插入图片描述
程序执行到断点处停止,此时 i == 5, 屏幕上打印了0,1,2,3,4。


CTRL + F5

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

这个快捷键就是正常的执行程序,不进行调试,CTRL + F5直接从main函数开头执行到结束,中间不停顿。

在这里插入图片描述


这里分享一篇博主的博客,它的文章中有非常详尽的快捷键介绍:

VS中常用的快捷键


调试的时候查看程序当前信息

查看临时变量的值

在调试开始之后,用于观察变量的值。
当我们按F10进入调试界面后,在上方任务栏中找到调试窗口,点开后有一个子菜单,再点来子菜单的窗口选项,就有各种查看变量的工具 :

  • 监视
  • 自动窗口
  • 局部变量
  • 调用堆栈
  • 内存
  • 反汇编
  • 寄存器

在这里插入图片描述

  1. 监视

在这里插入图片描述
在这里插入图片描述
在监视里可以添加你想观察的变量。内容可以包括(变量、表达式、地址)。
在这里插入图片描述
2. 自动窗口和局部变量

这两个工具的功能类似,都可以自动动态显示变量的变化值,由于他是自动显示的,有时无法一直观察某变量的变化。
在这里插入图片描述
这里就没法观察到a的值了。


总结

调试一般都是使用监视来观察变量的变化。

  1. 调用堆栈

在这里插入图片描述
在这里插入图片描述
压栈过程


在这里插入图片描述

在这里插入图片描述

出栈过程

通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。


  1. 内存
    在这里插入图片描述
    在这里插入图片描述

在内存的搜索框中输入&arr然后回车就可以找到arr数组在内存中的空间地址的内容了。
在这里插入图片描述
这里一行代表了4个字节。(一个内存地址代表一个字节)
在这里插入图片描述
可以在这里调节一行显示几个字节的内容。

过内存,可以看到各个变量在内存中是如何变化的。


  1. 反汇编寄存器
    反汇编代码中需要使用到寄存器。

寄存器寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。

进入调试界面,右击鼠标,有转到反汇编选项。
在这里插入图片描述
在这里插入图片描述
阅读这些代码需要用到反汇编知识。


调试代码的实例

实现代码:求 1!+2!+3!

这里一步一步来,先实现 n!的阶乘。
在这里插入图片描述
然后计算出各个数的阶乘后,相加即可。

#include <stdio.h>
int main()
{
	int n = 3;
	int i = 0;
	int ret = 1;
	int sum = 0;
	for (n = 1; n <= 3; n++)//从1到3
	{
		for (i = 1; i <= n; i++)//计算n!
		{
			ret *= i;
		}
		sum += ret;//阶乘的和
	}
	printf("%d ", sum);
	return 0;
}

大家观察一下上述代码,有没有什么不妥的地方,我们来看看打印结果:
在这里插入图片描述
我们发现结果与正确结果不相同,明明我们求一个数阶乘的代码没有问题啊,可是错误在哪呢?这时就需要使用到调试来进行排错了。
调试
按F10进入调试界面,打开监视观察各个变量的值。
在这里插入图片描述
在未进入循环时各个值。


在这里插入图片描述

当第一层for循环完,ret 中为1的阶乘,值为1。


在这里插入图片描述
当第二层for循环完,ret 中为2的阶乘,值为2。


在这里插入图片描述
当第三层for循环完,ret 中为3的阶乘,值为12。
此时,ret中的值发生异常,说明程序出错在第三次for循环内部,当我们重新调试时,
在这里插入图片描述
从第二层for循环进入到第三层for循环时,发现ret 的初始值为2,为上一次循环遗留下的值,所以该程序问题在于每次计算一个数的阶乘时,需要把ret的值重新初始化为1。
修改:

#include <stdio.h>
int main()
{
	int n = 3;
	int i = 0;
	int sum = 0;
	for (n = 1; n <= 3; n++)//从1到3
	{
		int ret = 1;
		for (i = 1; i <= n; i++)//计算n!
		{
			ret *= i;
		}
		sum += ret;//阶乘的和
	}
	printf("%d ", sum);
	return 0;
}

在这里插入图片描述
这就是一次完整的写代码及调试纠错的过程。在自己编写代码时可以参考步骤。


如何写出好(易于调试)的代码

优秀的代码

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

常见的coding技巧:

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

示范:

模拟实现库函数:strcpy

这里先了解一下strcpy 函数,在cplusplus.com网站上搜索一下这个函数。
在这里插入图片描述
从函数功能介绍来看,这个函数是将一个字符数组的内容复制给另一个字符数组,直到遇到’\0’后结束。第一个参数数组是被放入字符的数组,第二个参数数组是要复制的数组。
在这里插入图片描述

根据上述描述先实现一下核心功能,这里命名为my_strcpy函数。
在这里插入图片描述

这里可以用while循环把arr1的每一位复制到arr2数组中,最后再给arr2赋值一个’\0’。

#include <stdio.h>
void my_strcpy(char* dest, char* src)
{
	while (*src)
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = '\0';
}
int main()
{
	char arr1[] = "abcdefg";
	char arr2[10];
	my_strcpy(arr2, arr1);
	printf("%s\n", arr2);
	return 0;
}

在这里插入图片描述

思考:这样写就可以了吗?能否对代码进行再优化一下?
修改:

#include <stdio.h>
void my_strcpy(char* dest, char* src)
{
	while (*dest++ = *src++)//这样是不是更简洁
	{
		;
	}
}
int main()
{
	char arr1[] = "abcdefg";
	char arr2[10];
	my_strcpy(arr2, arr1);
	printf("%s\n", arr2);
	return 0;
}

在这里插入图片描述

思考:这样写就完善了吗?如果函数传参传来的是空指针(野指针)怎么办?
在这里插入图片描述
给函数传空指针(NULL),程序就崩溃了。
为了避免他人在使用my_strcpy函数时万一传空指针,就需要在函数内部判断一下传过来的参数。
修改:

#include <stdio.h>
void my_strcpy(char* dest, char* src)
{
	if(dest == NULL || src == NULL)//判断是否为空指针
		{
			return;
		}
	while (*dest++ = *src++)
	{
		;
	}
}
int main()
{
	char arr1[] = "abcdefg";
	char arr2[10];
	my_strcpy(arr2, arr1);
	printf("%s\n", arr2);
	return 0;
}

在这里介绍一个库函数assert(断言),可以判断参数的真假。
在这里插入图片描述
在这里插入图片描述

这样就又可以对上述代码进行修改了。

#include <stdio.h>
#include <assert.h>
void my_strcpy(char* dest, char* src)
{
	assert(dest && src);
	while (*dest++ = *src++)
	{
		;
	}
}
int main()
{
	char arr1[] = "abcdefg";
	char arr2[10];
	my_strcpy(arr2, arr1);
	printf("%s\n", arr2);
	return 0;
}

在这里插入图片描述

在这里插入图片描述
这样在传参时不小心传入空指针时,程序就能及时的检测和让程序停止下来了。
当完善到这里的时候,就不要以为代码已经很完善了,我们对比一下my_strcpy和strcpy的函数参数和返回值。
在这里插入图片描述
这里my_strcpy函数没有设计返回值,而strcpy函数返回了一个**char*的指针,我们思考一下,把一个源数组的内容复制给目标数组,那返回的地址应该是目标数组的地址才更符合实际。
这个问题解决了,再看一下第二个参数的类型不完全相同,strcpy函数的第二个参数前加了一个
const** 。
const是什么意思呢?

const含义:只要一个变量前用const来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着“只读”(readonly)。

const规则: const在谁后面谁就不可以修改,const在最前面则将其后移一位;const修饰一个变量时,一定要给这个变量初始化,若不初始化,在后面也不能初始化。

在这里插入图片描述

const作用:

1:可以用来修饰变量,修饰函数参数,修饰函数返回值,且被const修饰的东西,都受到强制保护,可以预防其它代码无意识的进行修改,从而提高了程序的健壮性(是指系统对于规范要求以外的输入能够判断这个输入不符合规范要求,并能有合理的处理方式。ps:即所谓高手写的程序不容易死);
2:使编译器保护那些不希望被修改的参数,防止无意代码的修改,减少bug;
3:增强代码的可读性,给读代码的人传递有用的信息,声明一个参数,是为了告诉用户这个参数的应用目的。

当const 修饰指针时,有两种情况:

  1. const 在*左边
#include <stdio.h>
int main()
{
	int n = 10;
	int m = 100;
	const int* p = &n;//const 和 int 位置可互换
	//*p = 20;
	p = &m;
	printf("%d\n", *p);
	return 0;
}

在这里插入图片描述
在这里插入图片描述
结论:
当const 在 * 左边,*p不能修改,也就是p指向的内容,不可以通过p来改变了。但是p是可以改变的,p可以指向其他的变量。

  1. const 在*右边
#include <stdio.h>
int main()
{
	int n = 10;
	int m = 100;
	 int* const p = &n;
	*p = 20;
	//p = &m;
	printf("%d\n", *p);
	return 0;
}

在这里插入图片描述
在这里插入图片描述

结论:
当const 在 * 右边,p不能修改,也就是p不能再指向其他变量,但是*p可以修改,也就是可以通过解引用p来改变p指向的变量的值。


通过对const有了深入了解,我们再次对my_strcpy函数进行修改:

#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* dest, const char* src)
{
	assert(dest && src);
	char* ret = dest;
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}
int main()
{
	char arr1[] = "abcdefg";
	char arr2[10];
	printf("%s\n", my_strcpy(arr2, arr1));
	return 0;
}

在这里插入图片描述
这就是一个完整的对库函数strcpy的模拟实现,你以后设计函数时,也可以根据我的思路,进行思考实现。


练习:

模拟实现一个strlen函数

这里给大家一个相似的练习,可以参照my_strcpy函数的思路来编写代码,后面还放上了参考代码以供大家对照。
参考代码:

#include <stdio.h>
#include <assert.h>
int my_strlen(const char* str)
{
	int count = 0;
	assert(str != NULL);
	while (*str)//判断字符串是否结束

	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	const char* p = "abcdef";
	//测试
	int len = my_strlen(p);
	printf("len = %d\n", len);
	return 0;
}

编程常见的错误

编译型错误

直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。

如代码的拼写错误,少写了分号,少打空格、括号之类的。
在这里插入图片描述

链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。

就像这样:
在这里插入图片描述
这么写会把读代码的人气死吧,hhh。

运行时错误

借助调试,逐步定位问题。最难搞。


本篇关于实用调试技巧的讲解就到此结束了,感兴趣的的小伙伴点点赞,点点关注,谢谢大家的阅读哦!!!
几千字长文写下来博主也不容易,点点赞吧!😘


下一篇将会进入C语言进阶的学习,会对浮点数在内存中的存储方式进行详细介绍,点点关注,后期不错过哦。😘
你们的鼓励就是我的动力,欢迎下次继续阅读!!!😘😘😘

  • 28
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
### 回答1: C语言编程技巧CHM,即C语言编程技巧帮助文档(Compiled HTML Help),是针对C语言编程的技巧和知识进行整理和总结形成的帮助文档。 C语言是一种广泛应用于系统软件和应用软件开发的高级编程语言。在C语言编程中,掌握一些技巧和知识可以提高代码的效率和可读性。编程技巧CHM可以对这些技巧进行详细讲解和演示,帮助初学者快速入门,同时也为有经验开发者提供更深入的理解和应用。 编程技巧CHM中可能包含的内容有:常用的编码规范和命名规则、优化和调试技巧、常用算法和数据结构等。通过学习和掌握这些技巧开发者可以更好地组织和设计程序,提高代码的质量和可维护性。 CHM格式的帮助文档具有良好的交互性和可扩展性,可以方便地进行搜索和索引,同时也可以方便地添加自定义的内容和链接。使用CHM格式的编程技巧文档,开发者可以在需要时快速查询和查找相关的内容,提高学习和开发效率。 总之,C语言编程技巧CHM是一种在C语言编程中非常有用的资源,能够帮助开发者更好地理解和应用C语言技巧和知识,提高代码的质量和效率。无论是初学者还是有经验开发者,都可以从中受益。 ### 回答2: C语言编程技巧CHM是一本关于C语言编程技巧的电子书,CHM是一种帮助文件格式。本书主要介绍了C语言编程中一些常用的技巧和方法,旨在帮助读者提高编程效率和代码质量。 首先,本书介绍了一些常用的C语言编程技巧,例如如何优化代码、避免一些常见的编程陷阱等。这些技巧可以帮助读者更加熟悉C语言的特性,以及掌握一些高效而安全的编程方法。 其次,本书还介绍了一些与C语言编程相关的工具和库。例如,介绍了如何使用C语言的标准库函数和数据结构,如何使用常用的开发工具,如编译器和调试器等。这些工具和库可以帮助读者更好地进行C语言编程,并提高开发效率。 此外,本书还讲解了一些常用的设计模式和算法。这些设计模式和算法是C语言编程中的一些常用技巧,可以帮助读者更好地解决实际编程中的问题。例如,介绍了常用的排序算法、搜索算法以及一些常用的数据结构等。 总而言之,C语言编程技巧CHM是一本对C语言编程非常实用的电子书。通过阅读本书,读者可以学习和掌握一些高效的C语言编程技巧和方法,提高自己的编程水平和能力。无论是初学者还是有一定编程经验的人都可以从本书中受益,为自己的C语言编程之路打下坚实的基础。 ### 回答3: C语言编程技巧是指在使用C语言进行编程时,运用一些技巧和方法来提高代码的效率和可读性。下面简要介绍几个常用的C语言编程技巧。 首先是代码注释。良好的代码注释能够提高代码可读性,便于他人理解和维护代码。在编写C语言代码时,应当养成良好的注释习惯,对于每个函数、变量以及复杂的操作,都应有相应的注释来解释其功能和作用。 其次是错误处理和异常处理。在编写代码时,应养成及时捕捉和处理错误的习惯,避免因漏掉错误处理而导致代码的异常运行或崩溃。对于可能出现异常的情况,可以使用条件判断语句来捕捉并处理错误,保证程序的稳定性和健壮性。 另外,利用函数和模块化编程也是提高C语言编程效率的重要技巧。将代码逻辑分成多个函数,使得每个函数只完成特定的功能,降低代码的复杂性,提高代码的可维护性。可以将一些常用的功能封装成独立的模块,以便在其他项目中复用。 此外,还有一些常用的编程技巧,如避免全局变量的使用,使用宏定义提高代码的可读性,合理选择循环结构和条件语句等等。 总结来说,C语言编程技巧是一种提高代码效率和可读性的方法和技巧,包括良好的注释习惯、错误处理、模块化编程以及其他一些常用的编程技巧。这些技巧的有效运用可以提高代码的质量和开发效率,是每个C语言程序员需要掌握的基本技能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ks胤墨

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

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

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

打赏作者

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

抵扣说明:

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

余额充值