目录

  • 什么是bug
    • 世界上第一个bug
  • 调试是什么?有多重要?
    • 我们是如何写代码的?
    • 调试是什么?
    • 调试的基本步骤
  • debug和release的介绍
  • windows环境调试介绍
    • 学会快捷键
    • 调试的时候查看程序当前信息
  • 调试代码的实例
  • 如何写出好(易于调试)的代码
  • 编程常见的错误

前言:

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

什么是bug

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

世界上第一个bug

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

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

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

调试是什么?有多重要?

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

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

我们是如何写代码的?



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

调试是什么?

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

调试的基本步骤

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

debug和release的介绍

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

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

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

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

在上述代码所属的项目下分别产生debug和release的应用程序


从上图中两种不同版本的相同代码所产生的应用程序的大小却相差30kb。


而且两种版本的反汇编代码也有差异:
debug:

release:

release版本对于代码的优化,可用下列代码验证:

#include 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 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 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 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 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 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 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 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 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 #include 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 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 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 #include 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 #include 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语言进阶的学习,会对浮点数在内存中的存储方式进行详细介绍,点点关注,后期不错过哦。
你们的鼓励就是我的动力,欢迎下次继续阅读!!!