实用调试技巧——“C”

简介: 实用调试技巧——“C”

各位CSDN的uu们你们好呀,今天小雅兰的内容是实用调试技巧,其实小雅兰一开始,也不知道调试到底是什么,一遇到问题,首先就是观察程序,改改这里改改那里,最后导致bug越修越多,或者是问别人,反正不会调试,那么,通过这篇文章,小雅兰对于调试的认识就更上了一个台阶,现在,就让我们进入实用调试技巧的世界吧


什么是bug?


调试是什么?有多重要?


Windows环境调试介绍


一些调试的实例


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


编程常见的错误


什么是bug?


 在英文释义中,“BUG”常被用作形容昆虫,比如小虫、爬虫、臭虫、千年虫等,但一般不会单独使用,而是与其他单词组合使用,比如常见的“millennium bug”,翻译成中文就是千年虫或千禧虫的意思,再比如“lightning bug”,翻译成中文是萤火虫的意思,但在英文词汇中,有单独的单词锚定萤火虫,比如“firefly”。所以,它翻译成中文的含义并不固定,通常用来描述虫类动物。


 在计算机领域,行业内通常会将计算机上发生的一些问题用“BUG”来标注,比如系统缺陷、程序漏洞、轻微毛病、问题、故障等,而在一些固定的英文句式中,甚至还可以用来形容轻微的疾病、窃听器材等,但类似中文“萤火虫”的英文翻译一样,这类词汇一样有其他单词替代,通常指的就是“缺陷”或“漏洞”。


 基于语言、语种的不同,单词的定义、释义,以及应用场景也是不同的,在英文单词词汇中,BUG可以解释前文中提到的含义,但在国内应用过程中,它又衍生出了另一种含义,那就是“不可以思议的事”,比如:游戏出现BUG了吧、这个BOSS可以卡BUG、这人真厉害,出BUG啦,在这三种场景下出现的“BUG”,一般不是指缺陷、漏洞或昆虫的意思,虽然也有这方面的意思,但更大的作用是作为感叹词出现,一般帮助发言者表达自己的感叹、好奇、愤怒和迷惑等情绪,指遇到了自己无法理解或不可思议的事情。

20b9996ba4f943c0b1c508f41237a750.png

第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。


调试是什么?有多重要?


 编好程序后,用各种手段进行查错和排错的过程。


 作为程序的正确性不仅仅表现在正常功能的完成上,更重要的是对意外情况的正确处理。


 从心理学的角度考虑,开发人员和调试人员不应该是同一个人。


1. [experiment and adjust]∶试验并调整机器、仪器等

2. [shakedown test]∶在安装过程中对设备所作的试验工作

3. [debug]:对功能、程序等进行调整和实验验证

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


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


一名优秀的程序员是一名出色的侦探。


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


我们是如何写代码的呢?

81eb601a5614497da303730015e51c03.png

又是如何排查出现的问题呢?

042f82217bac417ababb5696a59b02b9.png


5f7680a10c164d75994de64e7ddb9d76.jpg

28bcb2b71c524c008939948667a69df3.jpg

拒绝-迷信式调试!!!!  


调试到底是什么?


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


调试的基本步骤


发现程序错误的存在

以隔离、消除等方式对错误进行定位

确定错误产生的原因

提出纠正错误的解决办法

对程序错误予以改正,重新测试

Debug和Release


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

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

下面,来看一段代码:  

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

411bfbc18e414b44803b85244148cd9d.png

e1dce7d37fdd42aead0cd3d0bcb6010c.png

如果真的去实操了一下的话,会发现Release版本的速度更快一些,且在Release版本下.exe文件所占空间大小比在Debug版本下小得多

然后,再来看看二者的反汇编比较

39d47bad256c4892948260bae94149da.png

01b191af81ed4fa8871efb8e62ac28a1.png

我们会发现,在Debug版本下,反汇编的操作也多得多

所以我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。

那编译器进行了哪些优化呢?

下面,我们还是来看一段代码:

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

522b6cfbe0dc4cd8a818ceb447debbec.png

5abceeac458a4544a805e8b1ae744bee.png

d51cb170fa294886b6d1c41670a5ea5f.png

我们会发现:如果是在Debug版本下,在×86的平台下会死循环地打印hehe,在×64的平台下会报出Debug Error!的错误。


但是,在Release版本下,无论是在哪个平台,都没有死循环地打印hehe。


那他们之间有什么区别呢?


就是因为优化导致的。


在Release版本下是不能进行调试操作的,只有在Debug版本下才能进行调试!!!


Windows环境调试介绍


在环境中选择 debug 选项,才能使代码正常调试。


快捷键


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

F9 创建断点和取消断点 断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。

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

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

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

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


查看临时变量的值  在调试开始之后,用于观察变量的值。

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

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

查看汇编信息  在调试开始之后,有两种方式转到汇编:

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

 (2)第二种方式: 可以切换到汇编代码。

查看寄存器信息  可以查看当前运行环境的寄存器的使用信息。

多多动手,尝试调试,才能有进步。


一定要熟练掌握调试技巧。

初学者可能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;
}

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

44237660b4aa466fbe2bda394e19e478.pngwhy?

 这里我们就得找我们问题。

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

195b68c13bad4edc9b6f1508d0c2a1ae.png195b68c13bad4edc9b6f1508d0c2a1ae.png

调试过后发现变成了这样,所以我们必须要把ret在循环内部赋值成1

那么,正确的代码就应该是这样:

#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;
    ret = 1;
    for (j = 1; j <= i; j++)
    {
      ret *= j;
    }
    sum += ret;
  }
  printf("%d\n", sum);
  return 0;
}

e06d79c7cbf841d79bf4524c7623b129.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;
}

b4820b915bfb48289ae701344b991093.png

我们会看到,这个程序已经死循环了,可能有些人就会想:这个程序难道不是数组越界了吗,怎么会死循环呢,为什么不报错呀???我们带着一肚子疑惑,来研究这段代码。

806d8813650c40f9b5e7682f0ea2fd4a.png

原理:


i和arr是局部变量,局部变量是放在栈区上的

栈区内存的使用习惯是:先使用高地址处的空间,再使用低地址处的空间

数组随着下标的增长,地址是由低到高的

这段代码的的确确是越界访问了,但是,它忙着死循环,没时间报错,这段代码的运行结果是和环境相关的


C语言在写代码的时候,容易出现的一些错误,避坑指南——《C陷阱和缺陷》


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


优秀的代码:


代码运行正常

bug很少

效率高

可读性高

可维护性高

注释清晰

文档齐全

常见的coding技巧:


使用assert

尽量使用const

养成良好的编码风格

添加必要的注释

避免编码的陷阱

下面,我们来运用一下学过的这些知识

模拟实现库函数:strcpy

3722cc2b73a84f969e2720196e255812.png

strcpy函数返回的是目标空间的起始地址

#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
  assert(dest != NULL);
  assert(src != NULL);
  char* ret = dest;
  while (*dest++ = *src++)
  {
    ;
  }
  //把src指向的字符串拷贝到dest指向的数组空间,包括\0字符
  return ret;
}
int main()
{
  char arr1[] = "hello world";
  char arr2[20] = { 0 };
  printf("%s\n", my_strcpy(arr2, arr1));
  //链式访问
  return 0;
}

注意:

  • 分析参数的设计(命名,类型),返回值类型的设计
  • 这里讲解野指针,空指针的危害。
  • assert的使用,这里介绍assert的作用
  • 参数部分 const 的使用,这里讲解const修饰指针的作用
  • 注释的添加

const的作用

#include <stdio.h>
//代码1
void test1()
{
    int n = 10;
    int m = 20;
    int *p = &n;
    *p = 20;//ok
    p = &m; //ok
}
void test2()
{
     //代码2
    int n = 10;
    int m = 20;
    const int* p = &n;
    *p = 20;//err
    p = &m; //ok
}
void test3()
{
    int n = 10;
    int m = 20;
    int *const p = &n;
    *p = 20; //ok
    p = &m;  //err
}
int main()
{
    //测试无cosnt的
    test1();
    //测试const放在*的左边
    test2();
    //测试const放在*的右边
    test3();
    return 0;
}

const修饰指针变量的时候:


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

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

下面,我们来写一个练习:模拟实现一个strlen函数


其实在之前,我们就已经用计数器、指针-指针、递归的方法实现过这个函数,只是当时的代码写得不够好,现在,来重新实现一下。

#include<stdio.h>
#include<assert.h>
int my_strlen(const char* str)
{
  int count = 0;
  assert(str != NULL);
  while (*str != '\0')
  {
    count++;
    str++;
  }
  return count;
}
int main()
{
  char arr[10] = "abcdef";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;
}

aa5b07b1e50d4952962395ffbdc63b74.png

字符串传参,传的是首字符的地址。


编程常见的错误


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

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

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

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


好啦,小雅兰今天的内容就到这里啦,诶嘿,最近效率太低了,可能是学校水课太多了,让人心烦意乱吧,而且比较浪费时间,哈哈哈,只有C语言会让人快乐!!!

154c2ef665644ec787d5d307a1cdd47e.jpg

相关文章
|
2月前
|
程序员
调试技巧vs2022
调试技巧vs2022
|
10月前
调试技巧(2)
调试技巧(2)
调试技巧(2)
|
11天前
|
NoSQL 程序员 Linux
实用调试技巧(1)
实用调试技巧(1)
22 7
|
8月前
|
程序员 C语言
|
8月前
|
程序员 编译器
实用调试技巧(上)
实用调试技巧
|
10月前
|
C++
5 个非常实用的 vs 调试技巧
5 个非常实用的 vs 调试技巧
|
11月前
|
程序员 C语言
|
程序员 编译器