zl程序教程

您现在的位置是:首页 >  其他

当前栏目

【C/调试实用技巧】—作为程序员应如何面对并尝试解决Bug?

2023-03-07 09:46:30 时间

小菜坤日常上传gitee代码:https://gitee.com/qi-dunyan 关于C语言的所有由浅入深的知识,都存放在专栏【c】 ❤❤❤ 个人简介:双一流非科班的一名小白,期待与各位大佬一起努力! 推荐网站:cplusplus.com

目录

前言

我相信大家在写代码,或者刷题时,不可能每一次都是一次就能写出完美的不出错误的代码,如果真实这样的话,恭喜你,你是一个天才,并不需要进行本篇文章的学习,此文章是我整理的本人作为小白时期遇到的一些BUG,以及遇到这些BUG应如何去解决它,文章适用于新入门的小白,对于CV老司机就不适用啦。(?鸡哥护体) 通过本章的学习,将会帮助小白能自己调试,并解决一些简单问题!

BUG的前世今生

首先要解决它,就要先知道它知己知彼百战不殆,BUG是一个英语单词,本意是昆虫的意思

那么为何我们在写代码时,把错误称为BUG呢?这还真与昆虫有关。接下来大家可以看这么一个小故事。 1944年世界上第一台计算机马克1号诞生,之后在世界上第一位女程序员格蕾丝-霍普接手下,顺利改造成马克二号。 1946年的一天,霍普敲代码的时候发现计算机发生了故障,就在马克二号的继电器触点里,找到了一只被夹扁的小飞蛾。 而这只小虫子卡住了机器的运行,霍普顺手将飞蛾夹在工作笔记里,而备注的意思是臭虫,正是这一奇怪的称呼,奠定了Bug这个词在计算机世界的地位,此后,我们的生活便是处处与Bug打交道。

程序员应如何对待BUG

作为程序员的我们,应如何看待BUG呢?

在这里我们拒绝CV!调试的过程也是锻炼自己能力的一个过程!接下来将讲解有关调试的问题。

调试前言

调试,其实就是发现错误并修改减少错误的一个过程,就像画画一样,所谓画画,其实就是进行不断地修改,使画面更加和谐完美(美术生深有体会)最终成就一副完美的画面,调试也是如此,当代码遇到问题时,只有通过调试才能发现问题,并且解决问题!

调试步骤

调试的过程其实并不复杂,总结起来就以下几步,但做起来有时候确实要花费一些心思。后面会通过几个具体实例来讲解调试分析的具体步骤。

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

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. 文档齐全 常见的技巧:

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

接下来讲一下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的作用就显示出来了,简单粗暴,使用如下:

先到此为止,总之多调试,多分析,在写代码之前就有清晰的思路以及特殊边界的判断,然后进行编写。


end 生活原本沉闷,但跑起来就会有风!