【C语言】实用调试技巧

简介: 【C语言】实用调试技巧

👉什么是bug?👈


Bug作为一个英文单词,实际上它的中文含义并没有“漏洞”这个意思,原意是指小虫子、传染病、着迷以及窃听等等,但是在1947年9月9日之后,“Bug”就变成了错误或者漏洞的代称,据说当时一位计算机专家赫柏正在对17000个继电器进行程序的编辑,奈何突然发现整机停止运作。


等到工作人员查看了巨大的计算机整体后发现,原来是其中的一组继电器上的触点被一只飞蛾所妨碍了,当时因为这个触点的电压非常高,而飞蛾正好受到光和热的吸引撞在了上面,于是就被电死在触点上,于是赫柏就拿出一个纸条写上了“bug”,也就是虫子的意思,以此来代表“一个电脑程序中的错误”,最终“Bug”的说法也就流传了下来。


世界上的第一个BUG:

61db26de06fe4b7aa1fc60da43a92a00.jpg

那么我们怎么才能避免写出BUG呢?如果我们想要不写出BUG,就要提升我们调试代码的能力了。


👉调试是什么?有多重要?👈


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


顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。


而一名优秀的程序员就是一名出色的侦探,拥有强大的调试能力,能够快速地找出BUG。


每一次调试都是尝试破案的过程。


相信大家未来都是一名优秀的程序员!但是似乎现在,也有些人像下图那样子去写代码(BUG)。希望大家不要这样子,一定要写出优秀的代码。

9e63595cdc5044c3a4f56ce963d585e6.jpg


而且排查问题也不应该下图那样,一顿乱弄,然后自己都不知道错在哪里和对在哪里。所以我们一定要学会调试代码。

6665caffd22c4b81a50627997ffff54e.jpg


调试是什么?


调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。


调试的基本步骤


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


Debug和Release的介绍


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

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


代码示例:


#include <stdio.h>
int main()
{
  char* p = "hello bit.";
  printf("%s\n", p);
  return 0;
}


上述代码在Debug环境的结果展示:

1a56cfa9246f46f89ba279288eaee873.png

上述代码在Release环境的结果展示:


35ca5d1541474a5aba1773311bead50a.png


Debug和Release反汇编展示对比:

61e2b53188944062862a407a33442191.png

8ea45a9dfa894f2eb20da5532d9062f9.png

通过上面的对比,我们就可以发现Debug版本和Release版本有着明显的区别,这是因为Debug版本中含有调试信息,而且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模式去编译,程序没有死循环。这就是优化所带来的的明显差异。


👉Windows环境调试介绍👈


调试环境的准备

283ac5f2bd5f4871b457e68c9ed9af6d.png

在环境中选择Debug选项,才能使代码正常调试。如果选择Release选项,是无法进行调试的。


学会快捷键


最常使用的几个快捷键:


F5


启动调试,经常用来直接跳到下一个断点处。


F9


创建断点和取消断点。可以在程序的任意位置设置断点,

使得程序在想要的位置随意停止执行,继而一步步执行下去。


F10


逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。


F11


逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。


CTRL + F5


开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。


注意:如果你发现你按下F9、F10和F11键,都没有出现效果的话,试着同时按下Fn+F9/F10/F10就可以了。


F5的使用


F5一般是配合着F5使用的,F9设置一个断点,然后再按下F5跳到断点处。如果直接按下F5,代码直接运行结束了。举个例子:如果我想直接让箭头执行157行,那么我可以摁一下F9将断点设置在157行,再摁一下F5就能跳到157行了。

3033235c1748421aa6e3f3306013ffc0.png

cb29ebe1c87444699875423d2e095c35.png

F11的使用


F11也是逐语句执行,和F10的功能一样,当时F11可以让执行逻辑进入到函数内部。举个例子:进入调试后,当箭头执行157行时,再摁下F11就可以进入函数内部了。

7b0d1dce784046bf9098b9de1a7f3965.png

08ef2526bed247a1a92e9b34588eb3e6.png

调试的时候查看程序当前信息


1.查看临时变量的值


在调试开始之后,监视可用于观察临时变量的值。

304030a7a4d54f54882c07fab8221f4c.png

c6e02504282b471abbd796459e34a512.png


监视是调试是最最最常用的功能,也是最好用的功能。无论你想查看变量的值,还是变量的地址,都可以使用监视来查看。只要输入变量的名称,就可以查看变量的相关信息了。


2.查看内存信息


在调试开始之后,内存用于观察内存信息。

6e2f975cea2047d8828ff1cc3f3e6d95.png


跳出内存窗口之后,可以调整显示的列数。因为整型变量是4个字节,所以我们将其调整为显示4列,可以更好的观察整型的数据。如下图所示:


f143a1e514dd4b51915273bf4780e22e.png


我们在内存窗口上输入&a,就能找到a的地址,也可以看到a的数据。如果想要观察其他变量,也可以重新输入。如下图所示:

86b9a8ec59a34f11a373318076ffe2dc.png


查看调用堆栈


在调试开始之后,用于查看调用堆栈。

2aca61f778f6482a9f246754ffe2059a.png


通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。


4.查看汇编信息


在调试开始之后,有两种方式转到汇编代码。


(1)第一种方式:右击鼠标,选择【转到反汇编】:

8e86c08e58c64dfc9bc123ced565502e.png


(2)第二种方式:

15c88607cc894dd1abb8649d2911e142.png

以上两种方式都可以查看程序的汇编代码。


查看寄存器信息

122ff29e8aa349d3b8415c6cb34c10a4.png


这样就可以查看当前运行环境的寄存器的使用信息。如果大家想对寄存器想有更加深入了解的话,可以看一下这篇博客👉函数栈帧的创建和销毁👈。相信看完之后,你会对函数栈帧、汇编代码和寄存器的了解会有一定的提升。


👉多动手,敢调试,才能有进步👈


  • 一定要熟练掌握调试技巧。
  • 初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写程序,但是80%的时间在调试。
  • 现在我们所学的都是一些简单的调试。 以后可能会出现很复杂调试场景:多线程程序的调试等。
  • 多多使用快捷键,提升效率


👉一些调试的实例👈


实例一


实现代码:求 1!+2!+3! …+ n! ;不考虑溢出


#include <stdio.h>
int main()
{
  int i = 0;
  int sum = 0;//保存最终结果
  int n = 0;
  int ret = 1;//保存n的阶乘
  scanf("%d", &n);
  for (i = 1; i <= n; i++)
  {
    int j = 0;
    for (j = 1; j <= i; j++)
    {
      ret *= j;
    }
    sum += ret;
  }
  printf("%d\n", sum);
  return 0;
}

b986797455ea44598876005247cb030c.png


这时候,如果我们输入3,期待输出9,但是实际输出的是15。

这是为什么呢?我们就要找出哪里出了问题了。


  1. 首先推测问题出现的原因。初步确定问题可能的原因最好。
  2. 实际上手调试很有必要。
  3. 调试的时候我们要心里有数。


调试的时候,一定要把监视窗口调出来,方便我们观察每一个变量的值。通过调试,我们可以发现,当 i = 1 , 2 的时候,都没有什么问题。但是当 i = 3 的时候,结果就出问题了。原来问题是处在求阶乘的时候,我们没有将 ret 重新赋值为1,从而导致 ret 还保留着上一次阶乘的结果,然后就出现了问题。在这里,就向大家演示了一次调试代码和找 BUG 的过程,希望大家能学会。

2d33625376f049ba9f54e17bd0c5535a.png


代码修改:


#include <stdio.h>
int main()
{
  int i = 0;
  int sum = 0;//保存最终结果
  int n = 0;
  scanf("%d", &n);
  for (i = 1; i <= n; i++)
  {
    int ret = 1;//保存n的阶乘
    int j = 0;
    for (j = 1; j <= i; j++)
    {
      ret *= j;
    }
    sum += ret;
  }
  printf("%d\n", sum);
  return 0;
}

b731d65603814339a87aa79989016c56.png


实例二

#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;
}


上面代码的输出结果会是什么呢?我相信很多人,会说程序崩溃了,因为数组越界了。但是其实不是,而是死循环地打印 hehe。那为什么是这样子呢?请看下图:


711956d0026e4f3da970aed2dc371917.png

上面的代码是有意为之的,大家平时一定不要这么写代码,避免数组越界。


👉如何写出好(易于调试)的代码👈


优秀的代码:


  1. 代码运行正常
  2. bug很少
  3. 效率高
  4. 可读性高
  5. 可维护性高
  6. 注释清晰
  7. 文档齐全


    常见的coding技巧:


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


    一个漂亮的示例


    模拟实现strcpy函数


    strcpy函数的原型如下:


    char * strcpy ( char * destination, const char * source );


    0672ff07277d4872a82a6a633ba43a33.png


    代码示例:


    #include <stdio.h>
    #include <assert.h>
    char* my_strcpy(char* dst, const char* src)
    {
      assert(dst && src);
      char* ret = dst;//借助ret记住dst的首元素地址
      while (*dst++ = *src++)
      {
        ;
      }
      return ret;
    }
    int main()
    {
      char arr1[] = "******************";
      char arr2[] = "hello Joy";
      printf("%s\n", my_strcpy(arr1, arr2));
      return 0;
    }
    


    1.assert的作用


    assert是断言的意思,如果assert括号内的表达式为真,就什么事都不会发生。但是如果assert括号内的表达式为假,那么就会直接给你报错,哪行哪行出现了上面错误。见下图示例:


    7eed3f5fd94b4c3cba9afce92c1595c7.png

    2.函数返回值类型的设计


    其实上面的 my_strcpy 函数的返回值类型也可以是 void 类型,那为什么我们不把 my_strcpy 函数的返回值类型设置为 void 类型呢?因为将返回值类型设置为 char* 类型,可以实现函数的嵌套调用。


    比如上面的printf("%s\n", my_strcpy(arr1, arr2)); 语句,我可以直接将my_strcpy(arr1, arr2)的结果打印在屏幕上。


    3.’ \0 ’ 的拷贝


    上面的 my_strcpy 函数还有几个需要注意的点,第一,就是 ‘\0’ 的拷贝。当数组arr2的内容全部拷贝到数组arr1后,my_strcpy 函数会自动在后面加上 ‘\0’,作为字符串结束的标志。那现在就来调试起来看一下,是不是这样子的。


    演示代码:


    #include <stdio.h>
    #include <assert.h>
    char* my_strcpy(char* dst, const char* src)
    {
      assert(dst && src);
      char* ret = dst;//借助ret记住dst的首元素地址
      while (*dst++ = *src++)
      {
        ;
      }
      return ret;
    }
    int main()
    {
      char arr1[] = "******************";
      char arr2[] = "hello Joy";
      my_strcpy(arr1, arr2);
      printf("%s\n", arr1);
      return 0;
    }
    

    24387d7fdcd64ac1b76a4871a30652eb.png


    可以看到,my_strcpy将 ‘\0’ 也拷贝过去了,其实库函数strcpy也会将 ‘\0’ 拷贝过去。


    第二个需要注意的点就是,使用 strcpy 和 my_strcpy 函数一定要确定目的数组的空间足以容纳源头数组的内容,否则将会出现一些意想不到的问题。


    4.const的作用


    我们可以看到,上面 my_strcpy 的第二个参数 char* src 用了 const 来修饰。那么用 const 来修饰指针变量有什么意义呢?先告诉大家结论。


    结论:


    conts修饰指针变量

    • const 如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变。
    • const 如果放在 * 的右边,修饰的是指针变量本身,保证指针变量的内容不能修改,但是指针指向的内容可以通过指针来改变。


    举个例子:


    #include <stdio.h>
    int main()
    {
      int n = 10;
      int m = 20;
      int const* p = &n;// const int*p = &n 等价于 int const *p = &n
      //*p = 100; //error,const放在*的左边,不可以改变指针指向的内容
      p = &m;//可以改变指针变量本身的内容
      printf("%d\n", *p);
      return 0;
    }
    


    #include <stdio.h>
    int main()
    {
      int a = 10;
      int b = 20;
      int* const p = &a;
      //p = &b; //error,const放在*的右边,不可以改变指针变量本身的内容
      *p = 100;//可以改变指针指向的内容
      printf("%d\n", *p);
      return 0;
    }
    


    👉常见的错误👈


    编译型错误


    直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定,相对来说简单。

    5619a1e5da1d4f73a9a8bd43c92dd4b8.png


    这种错误是初学者最容易犯的错误,但是随着代码能力的提升,这种错误也会越来越少。


    链接型错误


    看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误

    bf1ba027d85c4b8596d7da97c4ab0798.png


    运行时错误


    运行时错误是最难搞的错误,需要借助调试,逐步定位问题。


    运行时错误可以由很多原因导致的,所以大家写代码的时候要细心一点。在这里就不跟大家讲解了,主要是借助调试来解决运行时错误。


    温馨提示:


    做一个有心人,积累拍错经验。

    4854dafa6569405f84b69e6a84d9d9f2.jpg


    👉总结👈


    本篇博客主要讲解了调试技巧、如何写出好的代码已经常见的错误等等。以上就是本篇博客的全部内容,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!!!💖💝❣️


    相关文章
    |
    6月前
    |
    存储 编译器 C语言
    【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存2
    【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
    |
    6月前
    |
    程序员 C语言 C++
    【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存1
    【C语言】VS实⽤调试技巧&(Debug和Release)监视&内存
    |
    程序员 C语言
    C语言--调试技巧(下)
    C语言--调试技巧(下)
    |
    5月前
    |
    安全 编译器 程序员
    【C语言】:VS实用调试技巧和举例详解
    【C语言】:VS实用调试技巧和举例详解
    50 1
    |
    5月前
    |
    存储 编译器 C语言
    C语言学习记录——调试技巧(VS2019环境下)
    C语言学习记录——调试技巧(VS2019环境下)
    53 2
    |
    6月前
    |
    程序员 C语言 C++
    C语言实用的调试技巧
    C语言实用的调试技巧
    48 3
    |
    6月前
    |
    NoSQL 编译器 C语言
    【C 言专栏】C 语言中的调试技巧与工具
    【5月更文挑战第6天】在C语言编程中,调试是必不可少的技能,涉及基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。高级技巧包括内存检查和性能分析。通过分析问题、设置断点、逐步调试和检查逻辑来解决错误。调试时需保持耐心,合理选用工具,记录过程,并避免过度调试。熟练掌握这些技能将提升代码质量和开发效率。
    152 0
    【C 言专栏】C 语言中的调试技巧与工具
    |
    6月前
    |
    程序员 C语言 C++
    C语言——调试技巧
    C语言——调试技巧
    |
    6月前
    |
    存储 程序员 编译器
    C语言第十三弹---VS使用调试技巧
    C语言第十三弹---VS使用调试技巧
    |
    存储 编译器 C语言
    C语言知识之结构体及实用调试技巧(二)
    C语言知识之结构体及实用调试技巧(二)
    86 0