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


    小菜坤日常上传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
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int c = a + b;
    	printf("%d\n", c);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这是在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在Debug版本下,是进行死循环的打印,但是在Release版本下,却能打印出13个hehe,(后面会通过调试实战讲到具体的死循环原因,在这里只需要知道,Release版本下,会对代码进行优化即可)。
    在这里插入图片描述

    调试实战

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

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

    F5
    启动调试,经常用来直接跳到下一个断点处。
    F9
    创建断点和取消断点
    断点的重要作用,可以在程序的任意位置设置断点。
    这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
    F10
    逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
    F11
    逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最长用的)。
    CTRL + F5
    开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

    我们在调试的时候,当然不仅仅是只看这两个快捷键,而是要通过查看程序的当前信息配合着使用,才能发挥最大作用。(Debug版本下进行调试)

    这里以上面的代码为例:解释下面代码在vs中运行死循环的原因。

    #include
    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
    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;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    以上这段代码没啥问题吧,运行起来打印出来的是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的作用就显示出来了,简单粗暴,使用如下:

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


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

  • 相关阅读:
    html 如何让网页变灰色
    腾讯云数据库SaaS致力于构建数据库分布式云,为更多更广的用户提供服务
    vue3+ts+uniapp实现小程序端input获取焦点计算上推页面距离
    为什么选择CodeEase?
    前端入门--JavaScript篇
    Notion 出现白屏的处理
    临时增加ASM diskgroup做备份用要及时取消,否则去掉DG 导致CRS 重启
    iOS pod repo push 报错 ld: file not found: libarclite_iphoneos.a 问题解决方案
    算法复杂度分析中的渐近分析(基于输入大小)
    java: 不兼容的类型: org.apache.xmlbeans.XmlObject无法转换为x2006.main.CTRow
  • 原文地址:https://blog.csdn.net/qq_60192898/article/details/127992638