目录
前言
BUG的前世今生
程序员应如何对待BUG
调试前言
调试步骤
Debug和Release的介绍
调试实战
快捷键以及程序信息的查看
遇到的常见错误类型介绍
如何写出好的代码
assert 与 const(C语言)
前言
我相信大家在写代码,或者刷题时,不可能每一次都是一次就能写出完美的不出错误的代码,如果真实这样的话,恭喜你,你是一个天才,并不需要进行本篇文章的学习,此文章是我整理的本人作为小白时期遇到的一些BUG,以及遇到这些BUG应如何去解决它,文章适用于新入门的小白,对于CV老司机就不适用啦。(🐔鸡哥护体)
通过本章的学习,将会帮助小白能自己调试,并解决一些简单问题!
BUG的前世今生
首先要解决它,就要先知道它知己知彼百战不殆,BUG是一个英语单词,本意是昆虫的意思
那么为何我们在写代码时,把错误称为BUG呢?这还真与昆虫有关。接下来大家可以看这么一个小故事。
1944年世界上第一台计算机马克1号诞生,之后在世界上第一位女程序员格蕾丝-霍普接手下,顺利改造成马克二号。
1946年的一天,霍普敲代码的时候发现计算机发生了故障,就在马克二号的继电器触点里,找到了一只被夹扁的小飞蛾。
而这只小虫子卡住了机器的运行,霍普顺手将飞蛾夹在工作笔记里,而备注的意思是臭虫,正是这一奇怪的称呼,奠定了Bug这个词在计算机世界的地位,此后,我们的生活便是处处与Bug打交道。
程序员应如何对待BUG
作为程序员的我们,应如何看待BUG呢?
在这里我们拒绝CV!调试的过程也是锻炼自己能力的一个过程!接下来将讲解有关调试的问题。
调试前言
调试,其实就是发现错误并修改减少错误的一个过程,就像画画一样,所谓画画,其实就是进行不断地修改,使画面更加和谐完美(美术生深有体会)最终成就一副完美的画面,调试也是如此,当代码遇到问题时,只有通过调试才能发现问题,并且解决问题!
调试步骤
调试的过程其实并不复杂,总结起来就以下几步,但做起来有时候确实要花费一些心思。后面会通过几个具体实例来讲解调试分析的具体步骤。
发现程序错误的存在
以隔离、消除等方式对错误进行定位
确定错误产生的原因
提出纠正错误的解决办法
对程序错误予以改正,重新测试
Debug和Release的介绍
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。我们进行的调试,都是在此版本进行的。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
在这里我们举个例子,以VS2019里的一个简单代码为例:
#include<stdio.h> int main() { int a = 10; int b = 20; int c = a + b; printf("%d\n", c); return 0; }
这是在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版本下,却能打印出13个hehe,(后面会通过调试实战讲到具体的死循环原因,在这里只需要知道,Release版本下,会对代码进行优化即可)。
调试实战
快捷键以及程序信息的查看
接下来先介绍几个最常使用的快捷键:
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点。
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最长用的)。
CTRL + F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
我们在调试的时候,当然不仅仅是只看这两个快捷键,而是要通过查看程序的当前信息配合着使用,才能发挥最大作用。(Debug版本下进行调试)
这里以上面的代码为例:解释下面代码在vs中运行死循环的原因。
#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; }
1.0 监视
首先是监视,它可以使我们在调试的时候清楚的知道变量随之的变化,从而进行判断所出的问题,并及时做出相应解决方案。
一定要在调试的环境下(按F10进行逐过程,或者F9+F5打断点调试),再去点击调试—窗口—监视
然后再监视窗口里,添加观察对象(变量),然后根据自己需要,进行调试,这里我进行逐语句调试,观察变量的变化。为什么会在Debug版本下死循环打印呢?
我们发现,当i==12时,i又一次变成了0,所以才进行死循环的打印。看来问题出在这里了(后面会讲到具体原因)
2.0 查看内存信息
在进入调试时,我们可以观察这些变量的内存信息,了解它们是如何在内存中存储的,有助于帮助我们进一步解决问题。
调试—窗口—内存
在这里为了便于观察,我们单机鼠标右键,将它改为16进制显示,并将列数改为4列。(一列就代表一个字节),如下:
通过他们内存中的地址存放信息,我们可以发现,arr数组与i中间间隔了两个整形元素大小,即8个字节,而数组arr一共有10个元素,也就是说,而arr[i],当i ==12时,刚好越界访问到第三个元素!arr[9]与arr[12]中间刚好也隔了两个整形空间。
即,可以这么来看!
我们发现,arr[12]中存放的就是i,arr[12]=0也就是相当于i=0,然后再进行循环,无穷无尽。
其实更加标准的答案是:i与arr都是属于局部变量,局部变量是存放在栈区,而栈区的空间使用是从高地址到低地址使用,所以i的地址要高于数组arr的地址,而数组又是随着下标的增长,地址也是由低到高变化,所以当数组arr越界访问适当时,是有可能访问到i的。
(ps:而其实在vs版本里,这里的越界访问空间经测试就是两个整形大小,在gcc环境下是1个整形大小,即在gcc环境下,arr[11]里存放的就是i)
当然,在调试窗口中也可以查看一些别的信息,如下,不过这里用不到,后面学的更加深入后,用到的会比较多一些。
遇到的常见错误类型介绍
在编程过程中,遇到的错误主要分为:编译型错误、链接型错误、运行时错误。
1、编译型错误
顾名思义,就是在我们编译代码时发生的一些错误,通常都是一些语法错误,对于这种错误我们根据报错提示,便可进行修改,如下所例:
2、链接型错误
发生在链接期间,在这里我们所犯的错误一般是标识符名不存在/未定义该标识符/未进行声明,或者拼写错误(很常见,假如函数名称很长,很容易写错名字)。如下:
3、 运行时错误
这一种比较头疼,就是代码可以运行,但是就是要么答案不对,要么程序崩掉(就比如调试实战里的案例),我们在做题时,有时候就会遇到,尤其是显示段错误,或者栈溢出等,这些都属于运行时的错误,遇到这种错误,要么我们就提前对一些边界值、特殊值进行考虑在内,想一想是不是进行越界访问了,还是对空指针进行解引用了,还是存在野指针的使用,还是等等等。。。或者加上断言(后面会讲),然后做题多了后就会长心眼了。
以上便是常见的错误类型。
如何写出好的代码
所谓优秀的代码,无非就以下几个特点:
1.代码运行正常
2. bug很少
3. 效率高
4. 可读性高
5. 可维护性高
6. 注释清晰
7. 文档齐全
常见的技巧:
使用assert
尽量使用const
养成良好的编码风格
添加必要的注释
避免编码的陷阱。
接下来讲一下assert与const
assert 与 const(C语言)
这里以strcpy函数为例,我们通过查看官网,发现strcpy是这样的:
const
这里我们细心的就会发现,第二个参数前面有一个const,那么const是用来干嘛的呢?它有什么作用呢?
这里我们以一个简单的例子来讲解一下:
#include<stdio.h> int main() { char arr[] = "abcd"; char* p = &arr; printf("%c\n", *p);//a *p = 'b'; printf("%c\n", *p);//b p = p + 2; printf("%c\n", *p);//c return 0; }
以上这段代码没啥问题吧,运行起来打印出来的是a b c,我们发现p存放的是arr首元素‘a’的地址,可以对*p,或者p的值进行修改,但是假如我们把const放在 *的左边,就会出现以下情况:
但假如把const放在 *右边,就会出现以下情况:
因此,我们有以下结论:
const放在*左边,则指针指向的变量的值不可修改,但是指针变量存放的地址可以修改,const放在 *右边,则指针指向的变量的值可以修改,但是指针变量存放的地址不可修改。
这样做是为了防止一些误操作,增加代码的健壮性,鲁棒性。
我总结了一个便于记忆的方法:可以这么来记:在C语言里,const后面的都不可修改,就比如const int *
p,这里*p不可修改,而int * const p,则p不可修改。
这里回过头来看strcpy就知道为什么加上const了,因为我们要把source的内容拷贝到destinatiion中去,所以这里source的内容肯定不能进行修改,否则拷贝的就不是原来的了。
assert
assert是断言所用,头文件需包含<assert.h>,也就是说,程序只要出现错误,就会立马弹出一个窗口进行警告。 特点:简单粗暴
我们经常用来判断传入的参数是否为空指针,或者用来断言一下别的必要条件,因为有时候,这个编译器不会报错,但是运行起来会直接崩掉,如下所例:
但是编译器并没有给出警告,只有当我们调试时,才会发生报错:
那么假如是一段特别长特别长的代码呢?那可就够我们吃一壶的了,不过这里assert的作用就显示出来了,简单粗暴,使用如下:
先到此为止,总之多调试,多分析,在写代码之前就有清晰的思路以及特殊边界的判断,然后进行编写。