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

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

目录


前言

BUG的前世今生

程序员应如何对待BUG

调试前言

调试步骤

Debug和Release的介绍

调试实战

快捷键以及程序信息的查看

遇到的常见错误类型介绍

如何写出好的代码

assert 与 const(C语言)

前言


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

通过本章的学习,将会帮助小白能自己调试,并解决一些简单问题!


BUG的前世今生

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


1.png


那么为何我们在写代码时,把错误称为BUG呢?这还真与昆虫有关。接下来大家可以看这么一个小故事。

1944年世界上第一台计算机马克1号诞生,之后在世界上第一位女程序员格蕾丝-霍普接手下,顺利改造成马克二号。

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

而这只小虫子卡住了机器的运行,霍普顺手将飞蛾夹在工作笔记里,而备注的意思是臭虫,正是这一奇怪的称呼,奠定了Bug这个词在计算机世界的地位,此后,我们的生活便是处处与Bug打交道。


2.png


程序员应如何对待BUG

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


3.png


在这里我们拒绝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版本下,该代码所对应的反汇编


4.png


下面这是在Release版本下改代码所对应的反汇编:


5.png


我们可以清晰地看到,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版本下,会对代码进行优化即可)。


6.png


调试实战


快捷键以及程序信息的查看

接下来先介绍几个最常使用的快捷键:


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打断点调试),再去点击调试—窗口—监视


7.png


然后再监视窗口里,添加观察对象(变量),然后根据自己需要,进行调试,这里我进行逐语句调试,观察变量的变化。为什么会在Debug版本下死循环打印呢?


8.png


我们发现,当i==12时,i又一次变成了0,所以才进行死循环的打印。看来问题出在这里了(后面会讲到具体原因)


9.gif


2.0 查看内存信息

在进入调试时,我们可以观察这些变量的内存信息,了解它们是如何在内存中存储的,有助于帮助我们进一步解决问题。

调试—窗口—内存


10.png


在这里为了便于观察,我们单机鼠标右键,将它改为16进制显示,并将列数改为4列。(一列就代表一个字节),如下:


11.png


通过他们内存中的地址存放信息,我们可以发现,arr数组与i中间间隔了两个整形元素大小,即8个字节,而数组arr一共有10个元素,也就是说,而arr[i],当i ==12时,刚好越界访问到第三个元素!arr[9]与arr[12]中间刚好也隔了两个整形空间。


12.png


即,可以这么来看!


13.png


我们发现,arr[12]中存放的就是i,arr[12]=0也就是相当于i=0,然后再进行循环,无穷无尽。


其实更加标准的答案是:i与arr都是属于局部变量,局部变量是存放在栈区,而栈区的空间使用是从高地址到低地址使用,所以i的地址要高于数组arr的地址,而数组又是随着下标的增长,地址也是由低到高变化,所以当数组arr越界访问适当时,是有可能访问到i的。


(ps:而其实在vs版本里,这里的越界访问空间经测试就是两个整形大小,在gcc环境下是1个整形大小,即在gcc环境下,arr[11]里存放的就是i)


当然,在调试窗口中也可以查看一些别的信息,如下,不过这里用不到,后面学的更加深入后,用到的会比较多一些。


14.png


遇到的常见错误类型介绍

在编程过程中,遇到的错误主要分为:编译型错误、链接型错误、运行时错误。

1、编译型错误

顾名思义,就是在我们编译代码时发生的一些错误,通常都是一些语法错误,对于这种错误我们根据报错提示,便可进行修改,如下所例:


15.png


2、链接型错误

发生在链接期间,在这里我们所犯的错误一般是标识符名不存在/未定义该标识符/未进行声明,或者拼写错误(很常见,假如函数名称很长,很容易写错名字)。如下:


16.png


3、 运行时错误

这一种比较头疼,就是代码可以运行,但是就是要么答案不对,要么程序崩掉(就比如调试实战里的案例),我们在做题时,有时候就会遇到,尤其是显示段错误,或者栈溢出等,这些都属于运行时的错误,遇到这种错误,要么我们就提前对一些边界值、特殊值进行考虑在内,想一想是不是进行越界访问了,还是对空指针进行解引用了,还是存在野指针的使用,还是等等等。。。或者加上断言(后面会讲),然后做题多了后就会长心眼了。


以上便是常见的错误类型。


如何写出好的代码

所谓优秀的代码,无非就以下几个特点:

1.代码运行正常

2. bug很少

3. 效率高

4. 可读性高

5. 可维护性高

6. 注释清晰

7. 文档齐全

常见的技巧:


使用assert

尽量使用const

养成良好的编码风格

添加必要的注释

避免编码的陷阱。

接下来讲一下assert与const


assert 与 const(C语言)

这里以strcpy函数为例,我们通过查看官网,发现strcpy是这样的:


17.png


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放在 *的左边,就会出现以下情况:


18.png19.png


但假如把const放在 *右边,就会出现以下情况:


20.png


因此,我们有以下结论:


const放在*左边,则指针指向的变量的值不可修改,但是指针变量存放的地址可以修改,const放在 *右边,则指针指向的变量的值可以修改,但是指针变量存放的地址不可修改。

这样做是为了防止一些误操作,增加代码的健壮性,鲁棒性。


我总结了一个便于记忆的方法:可以这么来记:在C语言里,const后面的都不可修改,就比如const int *

p,这里*p不可修改,而int * const p,则p不可修改。


这里回过头来看strcpy就知道为什么加上const了,因为我们要把source的内容拷贝到destinatiion中去,所以这里source的内容肯定不能进行修改,否则拷贝的就不是原来的了。


assert


assert是断言所用,头文件需包含<assert.h>,也就是说,程序只要出现错误,就会立马弹出一个窗口进行警告。 特点:简单粗暴


我们经常用来判断传入的参数是否为空指针,或者用来断言一下别的必要条件,因为有时候,这个编译器不会报错,但是运行起来会直接崩掉,如下所例:


21.png


但是编译器并没有给出警告,只有当我们调试时,才会发生报错:


22.png


那么假如是一段特别长特别长的代码呢?那可就够我们吃一壶的了,不过这里assert的作用就显示出来了,简单粗暴,使用如下:


23.png


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



相关文章
|
6月前
|
设计模式 算法 程序员
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
作为开发者,我们在日常开发过程中,往往会遇到反复修改bug的情况,而且不能一次性把代码写的完美无瑕,其实开发项目是一项复杂而富有挑战性的任务,即使经验丰富的程序员也难以在一次性编写完美无瑕地完成代码,我个人觉得一次性写好代码是不可能完成的事情。虽然在设计之初已经尽力思考全面,并在实际操作中力求精确,但程序员仍然需要花费大量时间和精力来调试和修复Bug。那么本文就来分享程序员需要反复修改Bug的原因,以及在开发中所面临的复杂性与挑战。
177 1
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
|
6月前
|
存储 敏捷开发 安全
软件开发有那些陷阱?如何避坑?
软件开发有那些陷阱?如何避坑?
70 4
|
6月前
|
IDE 安全 程序员
揭秘如何用C编写出无敌的程序代码,你绝对会后悔错过!
揭秘如何用C编写出无敌的程序代码,你绝对会后悔错过!
39 1
|
程序员
编程终极技能-调试(下)
编程终极技能-调试
|
6月前
|
程序员 Python
揭秘单步调试:掌握这一技能让你代码无懈可击
揭秘单步调试:掌握这一技能让你代码无懈可击
58 0
|
安全 NoSQL 程序员
我就算跳出去死外边也不会学【实用调试技巧/程序员内功修炼】
我就算跳出去死外边也不会学【实用调试技巧/程序员内功修炼】
39 1
|
消息中间件 算法 安全
开发者最怕遇到的代码报错
作为一名开发者,遇到代码报错是再平常不过的事情了。无论是在编写新代码还是修改现有代码时,都有可能出现各种各样的错误。有些错误可能只是简单的语法错误,而有些错误可能会导致整个程序无法正常运行。另外,结合阅读《实战总结|记一次消息队列堆积的问题排查》这篇文章,作者在工作中遇到的消息队列堆积的问题以及如何解决这一问题的过程,我深刻认识到了在开发过程中遇到问题准确排查的重要性,以及解决问题的策略和方法。那么接下来,就来聊一聊开发者最怕遇到的代码报错,以及如何有效地解决和避免这些问题。
294 2
开发者最怕遇到的代码报错
|
算法 程序员 编译器
当程序遇上困难:程序调试的艺术(VS)
当程序遇上困难:程序调试的艺术(VS)
69 0
|
程序员 编译器
编程终极技能-调试(上)
编程终极技能-调试
|
设计模式 新零售 供应链
一文教会你如何写复杂业务代码
这两天在看零售通商品域的代码。面对零售通如此复杂的业务场景,如何在架构和代码层面进行应对,是一个新课题。针对该命题,我进行了比较细致的思考和研究。结合实际的业务场景,我沉淀了一套“如何写复杂业务代码”的方法论,在此分享给大家。
28647 1
一文教会你如何写复杂业务代码
下一篇
无影云桌面