C语言

8. C语言--调试

Posted on 2022-01-30,8 min read

8.调试

8.1 什么是bug?

程序错误,即英文的Bug,也称为缺陷、臭虫,是指在软件运行中因为程序本身有错误而造成的功能不正常、死机、数据丢失、非正常中断等现象。

史上的第一只 "Bug" ,真的是因为一只飞蛾意外走入一电脑而引致故障,因此Bug从原意为臭虫引申为程序错误。

程序错误参考资料 (baidu.com)

8.2 调试是什么?有多重要?

8.2.1 调试概念

调试(英语:Debugging/Debug):又称排错,将编制的程序投入实际运行前,用手工或编译程序等方法进行测试,修正语法错误和逻辑错误的过程。是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

8.2.2 基本步骤

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

8.3 debug和release的介绍

8.3.1 Debug

调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序。并且程序中只有包含了额外的辅助信息才可以进行调试。

8.3.2 Release

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

8.3.3 总结

Release版本相较于Debug版本的可执行文件,空间更小,运行更快。


8.4 windows环境调试介绍

Linux系统中使用gdb(GNU symbolic debugger)


8.4.1 快捷键

快捷键 操作
Ctrl + Alt + P 附加到进程
F10 调试单步执行
F5 开始调试
Shift + F5 停止调试
Ctrl + Alt + Q 添加快捷匹配
F9 设置或删除断点

VisualStudio2019快捷键汇总_Dahlin哥's 博客-CSDN博客_vs快捷键

8.4.2 窗口

重点:某些窗口只有当调试起来(快捷键 F10F11)之后才会显示。


8.4.3 断点

设置断点,如果开始执行(不调试),程序直接执行结束,并不会在断点处暂停。所以使用断点必须调试执行才能生效。断点之前的程序正常执行,到断点处暂停。使用断点窗口,可以方便的查看多个源文件中的断点位置。


添加条件

  • 在循环中设置断点的条件,可以循环到一定的次数之后进行暂停。



其他

  • 使用F5移动到下一个断点。

8.4.4 监视

监视各个变量值的变化。可以自己添加需要监视的标识符和不需要监视的标识符。


此外还有自动监视和局部变量窗口。自动监视窗口可以自动检测需要监视的变量。局部变量窗口显示局部变量的变化。

8.4.5 调用堆栈

函数的调用逻辑。

8.4.6 其他


内存窗口,可以设置每行显示的列数。

8.5 调试实例

8.5.1 阶乘之和

找出代码中的bug

int main()
{
  int n = 3;
  int sum = 0;
  int mul = 1;
  for (int i = 1; i <= n; i++)
  {
    for (int j = 1; j <= i; j++)
    {
      mul *= j;
    }
    sum += mul;
  }
  printf("%d\n", sum);
  return 0;
}


// bug原因,mul变量在每次循环之后并没有重置为1

8.5.2 数组越界死循环

寻找原因

int main()
{
  int i = 0;
  int arr[10] = { 0 };
  for ( i = 0; i <= 12; i++)
  {
    arr[i] = 0;
    printf("Hello\n");
  }
  return 0;
}
// vs2013~2019出现死循环
// vs2022中正常循环12次,并没有出现数组访问越界错误
// 下图为vs2022运行结果图



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

8.6.1 优秀的代码

  • 代码正常运行
  • bug很少
  • 效率高
  • 可读性高
  • 可维护性高
  • 注释清晰
  • 文档齐全

8.6.2 常见的coding技巧

  • 使用assert
  • 尽量使用const
  • 养成良好的编码风格
  • 添加必要的注释
  • 避免编码的陷阱

8.6.3 实例

模拟实现库函数strcpy

介绍

  • 声明
char *strcpy(char *dest, const char *src)
  • 参数

    • dest: 用于存储复制内容的目标数组
    • src: 要复制的字符串
  • 返回值:返回指向目标字符串的指针
    代码

  • 库函数

    #include <string.h>
    int main() {
      char arr1[20] = { 0 };
      char arr2[] = "Hello World!";
      strcpy(arr1, arr2);
      printf(arr1);
      return 0;
    }
    // 需要在源文件最开始加入#define _CRT_SECURE_NO_WARNINGS
    


  • 自实现

    void my_strcpy(char* dest, char* sour)
    {
      while (*sour != '\0')
      {
        *dest = *sour;
        dest++;
        sour++;
      }
      *dest = *sour;
    }
    
    int main() {
      char arr1[20] = { '\0'};
      char arr2[] = "Hello";
      my_strcpy(arr1, arr2);
      printf(arr1);
      return 0;
    }
    
  • 优化1

    void my_strcpy(char* dest, char* sour)
    {
      while (*sour != '\0')
      {
        *dest++ = *sour++;
      }
      *dest = *sour;
    }
    
  • 优化2

    void my_strcpy(char* dest, char* sour)
    {
      while (*sour)
      {
        *dest++ = *sour++;
      }
      *dest = *sour;
    }
    
  • 优化3

    void my_strcpy(char* dest, char* sour)
    {
      while (*dest++ = *sour++); 
      // 先将*sour赋值给*dest
      // 获得整个表达式的结果
      // 并判断整个表达式的结果是否为假
      // 如果为真 再对dest和sour进行自增
    }
    
  • 优化4

    void my_strcpy(char* dest, char* sour)
    {
      assert(src != NULL); // 断言
      assert(dest != NULL); // 断言
      while (*dest++ = *sour++); 
    }
    // assert的使用需要引用头文件<assert.h>
    // assert中的表达式类似于if中的表达式
    
  • 优化5

    void my_strcpy(char* dest, const char* sour)
    {
      assert(src != NULL);
      assert(dest != NULL);
      while (*dest++ = *sour++); 
    }
    // const char* sour
    // sour指向的对象的内容不能被修改
    // 防止将dest中的内容复制到sour中
    // 即使发生了问题,发生的也只是语法错误,也容易找出并解决问题
    // 另外const char* sour中的const只需要在*前面,并不一定要在char前面
    // 即:char const * sour 但一般char*是一种类型,不拆开
    
    • 常量指针和指针常量

      int main()
      {
        int m = 100;
        int n = 10;
      
        //const int* p = &n; // 指针变量指向的对象不能改变
        //*p = 100; // ×
        //p = &m; // √
      
        //int* const p = &n; // 指针变量的值不能改变
        //p = &m; // ×
        //*p = m; // √
      
        //const int* const p = &n; // 指针变量和指针变量的值都不能改变
        //p = &m; // ×
        //*p = m; // ×
      
        //const int* p; // 称为常量指针
        //int* const p; // 称为指针常量
        return 0;
      }
      
  • 优化6

    char* my_strcpy(char* dest, const char* sour)
    {
      assert(src != NULL);
      assert(dest != NULL);
      char* ret = dest;
      while (*dest++ = *sour++);
      return ret; // 返回目标空间的其实地址(为了更好的看到目标地址发生的变化)
    }
    // 链式访问功能
    

模拟实现库函数strlen

  • 自实现

    size_t my_strlen(const char *s)
    {
      assert(s);
      size_t len = 0;
      while (*s++)len++;
      return len;
    }
    // size_t 相当于 unsigned int
    
    int main()
    {
      char s[] = "hello";
      int len = my_strlen(s);
      printf("%d\n", len);
      return 0;
    }
    
  • 参考代码

    size_t __cdecl strlen (
            const char * str
            )
    {
            const char *eos = str;
    
            while( *eos++ ) ;
    
            return( eos - str - 1 );
    }
    
    // 参考代码并不代表真实实现
    // __cdecl表示函数调用约定,不影响函数使用
    

8.7 编程常见的错误

错误类型

  • 编译型错误

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

  • 链接型错误

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

  • 运行时错误

    借助调试信息,逐步定位问题。难度最大。


下一篇: 7. C语言--结构体→