实用调试技巧

简介: 实用调试技巧

什么是bug?

1.png

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


调试是什么?有多重要?

调试是什么

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

调试这个词本质上是从硬件这个词来的,比如早期我们说的调试这个机器、调试这个硬件到底能不能工作。后来引申到我们程序里面也是一样的,程序里面也会出现一些问题,这个动作就叫调试。


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

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


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


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


调试的基本步骤

发现程序错误的存在。

以隔离、消除等方式对错误进行定位。(屏蔽一块代码或者放出一块代码看看会不会出现问题,进而定位到错误产生的区域,然后再解决问题。)

确定错位产生的原因。

提出纠正错误的解决方法。

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


debug和release的介绍

2.png


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

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


当时Debug的时候,我们是可以进行调试的,我们是可以通过调试窗口来观察到的。因为这段代码在编译的过程中产生各种各样的调试信息,它把程序在运行过程的上下文环境的相关信息保留下来。

3.png


当我们改成Release版本时,请看:

4.png

这个时候我们是不可以进行调试的。此时我们按F10进行调试时,它可能是跳着走的,它压根就没有按照我们的逻辑来。一些值不能进行很好的观察。

5.png

Debug文件夹底下放的是编译出来的Debug版本的相关信息,调试版本的可执行程序;而Release文件夹底下放的是Release版本的相关信息,发布版本的可执行程序。

6.png


windows环境调试介绍

快捷键

这是VS2022中的快捷键:

7.png

F5


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


F9


创建断点和取消断点

断点的重要作用,可以在程序的任意位置设置断点

这样就可以使得程序在想要的位置随意停止断点,继而一步步执行下去

那段点有什么用吗?如果说我们有500行代码、5000行代码、甚至是50000行代码让我们进行调试,设置断点可以帮助我们提高调试的效率。

注意断点要打在有意义的地方。


F10


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


F11


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

CTRL+F5


开始执行不调试,如果你想要程序直接运行起来而不进行调试就可以直接使用。即使我们打断点程序也不会停下来。


当然VS中还有很多的快捷键,操作起来非常方便,这里就不进行一一列举。


注意:F10和F11处理函数的方式是截然不同的。

断点在多行代码、跨文件使用方面是非常方便的。

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

查看临时变量的值

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

注意只有按完F10进入调试状态之后才可以。

8.png

1.png

2.png

注意上图:当数组传参的时候,如果你想在形参的这一部分看到这个数组其实是看不到的,所以我们要添加一个逗号即(arr,12)。

3.png

4.png

5.png

6.png

接下来看反汇编:

7.png

8.png

反汇编可以看到我们的C语言代码翻译出来的汇编代码是什么样子的。

接下来看寄存器:

9.png

如果我们知道寄存器的名字,我们也可以在监视窗口看到,请看:

10.png

以上是怎么来查看程序执行过程中上下文环境中的变量的值、以及它的内存里的值等等。

下面是调用堆栈:

11.png

用栈的一种这样的形式模拟出来函数的一个调用逻辑,当未来看到函数调用堆栈写的是这样一个逻辑的时候,就能够清楚的看到函数调用的一个逻辑是什么。倘若工程非常大,有几千甚至上万行代码,调试起来逻辑非常复杂的时候,我们可以用调用堆栈来看到当前是怎样的一个调用关系的。

上这些只是简单的调试技巧,需要的是我们多多动手尝试调试,才能有所进步。


调试实例

实例一

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

在这之前我们先来算n!:

//求n!
#include<stdio.h>
int main()
{
  int n = 0;
  int ret = 1;
  int i = 0;
  scanf("%d", &n);
  for (i = 1; i <= n; i++)
  {
  ret *= i;
  }
  printf("%d\n", ret);
  return 0;
}

12.png

//需要注意的点是这里的ret进行初始化时一定要初始化为1,而不是0。



下面就来看一下求1!+2!+3!+…+n!:

#include<stdio.h>
int main()
{
  int n = 0;
  int i = 0;
  int ret = 1;
  int sum = 0;
  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;
}
//这里要注意的点就是每次循环时要重新对ret进行初始化。

13.png

14.png

实例二

#include<stdio.h>
int main()
{
  int i = 0;
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  for (i = 0; i <= 12; i++)
  {
  printf("hehe\n");
  arr[i] = 0;
  }
  return 0;
}

这段代码会出现死循环的情况:

15.png

16.png

这个地方

17.png

这个地方错误的本质数组的越界导致的。


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

优秀的代码

1.代码运行正常

2.bug很少

3.效率高

4.可读性高

5.可维护性高

6.注释清晰

7.文档齐全


常见的coding技巧:


1.使用assert

2.尽量使用const

3.养成良好的编程风格

4.添加必要的注释

5.避免编码的陷阱


我们来模拟一下strcpy函数,然后不断优化:

#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
  while (*src != '\0')
  {
  *dest = *src;
  dest++;
  src++;
  }
  *dest = *src;
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);
  return 0;
}

进行优化:

#include<stdio.h>
//void my_strcpy(char* dest, char* src)
//{
//  while (*src != '\0')
//  {
//  *dest++ = *src++;
//  
//  }
//  *dest = *src;//拷贝\0
//}
void my_strcpy(char* dest, char* src)
{
  while (*dest++ = *src++)
  {
  ;
  }
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);
  return 0;
}

进行优化:

#include<stdio.h>
#include<assert.h>
void my_strcpy(char* dest, char* src)
{
  /*if (src == NULL || dest == NULL)
  {
  return;
  }*/
  //断言
  //assert中可以放一个表达式,表达式的结果如果为假,就报错,如果为真啥事也不发生
  //assert其实在release版本中优化掉了
  /*assert(src != NULL);
  assert(dest != NULL);*/
  assert(dest && src);
  while (*dest++ = *src++)
  {
  ;
  }
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char arr2[] = "hello";
  my_strcpy(arr1, arr2);
  printf("%s\n", arr1);
  return 0;
}
//所以未来使用指针之前像判断指针的有效性,我们可以用assert来进行断言
//assert不是仅仅只用来断言指针,一个变量的值,你不想它是什么,我们就可以用assert来进行断言。

进行优化:

#include<stdio.h>
#include<assert.h>
void my_strcpy(char* dest,const char* src)
{
  assert(dest && src);//断言指针的有效性。
  while (*dest++ = *src++)
  {
  ;
  }
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char* p = "hello";//p指向的常量字符串是不可以被修改的
  my_strcpy(arr1, p);
  printf("%s\n", arr1);
  return 0;
}
#include<stdio.h>
#include<assert.h>
void my_strcpy(char* dest, const char* src)//这里const修饰的是指针变量本身,但指针所指向的内容,可以通过指针来改变
//int num = 10;
//int* p = &num;
//int n = 1000;
//const修饰指针变量的时候
//1.const放在*的左边,const修饰的是指针所指向的内容,不能通过指针来改变量,不过通过指针来改变了;但是指针变量本身可以修改
// const int* p=&num;
// *p=20//err
// p=&n;//ok
// const int* p=&num;
//2.const放在*的右边,const修饰的指针变量本身,表示指针变量本身的内容不能够被修改,但是指针指向的内容,可以通过指针来修改
//int* const p=&num;
//*p=20;//ok
//p=&n;//err
{
  assert(dest && src);//断言指针的有效性。
  while (*dest++ = *src++)
  {
  ;
  }
}
int main()
{
  char arr1[20] = "xxxxxxxxxx";
  char* p = "hello";//p指向的常量字符串是不可以被修改的
  my_strcpy(arr1, p);
  printf("%s\n", arr1);
  return 0;
}

优化:

#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
  char* ret = dest;
  assert(dest && src);//断言指针的有效性。
  while (*dest++ = *src++)
  {
  ;
  }
  return ret;
}
int main()
{
  char arr1[20] = { 0 };
  char* p = "hello";//p指向的常量字符串是不可以被修改的
  //链式访问
  //当我们把一个函数的返回值作为另外一个参数它就实现了链式访问
  printf("%s\n", my_strcpy(arr1, p));
  return 0;
}


在这里我们考虑了const、考虑了指针的断言、考虑它的返回值类型,同时也包括\0的拷贝。


//模拟实现strlen
#include<stdio.h>
size_t my_strlen(const char* str)
{
  size_t count = 0;
  while (*str)
  {
  count++;
  str++;
  }
  return count;
}
int main()
{
  char arr[] = "helloworld";
  printf("%d\n", my_strlen(arr));
  return 0;
}
//我们也可以用指针-指针的方式来实现


编程常见的错误

编译型错误

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


链接型错误

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

18.png


运行时错误

借助调试,逐步定位问题,最难搞。

21.png

20.png

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

本文到此也就结束了,感谢各位!!!

目录
相关文章
拯救你的排版噩梦,搞定Deepseek到WPS的完美转换!
使用DeepSeek生成的文案复制到WPS后排版混乱?别担心,本文教你用LibreOffice解决此问题。首先下载并安装LibreOffice,然后将DeepSeek文案粘贴其中,保存为Word格式,最后用WPS打开,排版完美保留。简单四步,轻松搞定!
|
机器学习/深度学习 人工智能 供应链
探索未来技术前沿:人工智能与区块链的融合创新
【10月更文挑战第14天】 探索未来技术前沿:人工智能与区块链的融合创新
|
前端开发 开发者
@workspace 使用指南
我是一位前端开发工程师,使用通义灵码@workspace进行项目了解、问题解答及代码优化建议,效率提升80%。通过询问项目赔付方式、架构信息和特定文件的优化建议,快速掌握项目细节,极大提高开发效率。
|
存储 人工智能 数据格式
总说具身智能的数据太贵,鹏城实验室开源百万规模标准化数据集
【9月更文挑战第18天】鹏城实验室提出的ARIO(All Robots In One)标准,为具身智能领域带来了统一的数据格式、丰富的感知模态及多样化的真实与模拟数据,显著提升了数据集的质量与规模,助力智能系统更好地与物理世界互动。基于此标准构建的大规模数据集包含约300万个片段,覆盖258个系列和321,064个任务,极大地推动了具身智能的研究与发展。然而,该数据集也面临着存储需求高、系统互操作性及应用场景适应性等挑战。论文详情见:http://arxiv.org/abs/2408.10899。
399 12
|
消息中间件 存储 Kafka
Lindorm Ganos轨迹点快速聚合能力简介
本文介绍了Ganos时空数据库在Lindorm流引擎上的全新能力与最佳实践,帮助客户解决车辆网场景中轨迹点实时聚合生成轨迹线的能力。Lindorm Ganos实现了Lindorm宽表、流、计算等引擎在时空领域的打通,支持原生时空类型与多种时空算子,支持多种不同的时空索引,不仅可用于传统的周边查询,还面向了历史轨迹的查询分析、实时地理围栏查询、点面查询等更加复杂的业务需求。
使用阿里云身份证扫描识别接口案例—
使用阿里云身份证扫描识别接口案例—
|
SQL 分布式计算 大数据
阿里云大数据ACA及ACP复习题(511~520)
本人备考阿里云大数据考试时自行收集准备的题库,纯手工整理的,因为是纯手工整理解析所以可能出现答案打错的情况,题库是能够覆盖到今年7月份,应该是目前最新的,发成文章希望大家能一起学习,不要花冤枉钱去买题库背了,也希望大家能够顺利通关ACA和ACP考试(自己整理解析也需要时间,可能有更新不及时的情况哈)
|
机器学习/深度学习 编解码 人工智能
视频分辨率的历史发展,未来趋势是什么?
随着技术的不断发展,视频分辨率也在不断提高。视频分辨率是指视频图像中可显示的像素数量,通常表示为水平像素数和垂直像素数。它对于观看视频时的清晰程度和细节呈现有着重要的影响。 本文将会介绍视频分辨率的基本概念,包括单位、历史发展、标准和未来趋势。我们还会探讨视频分辨率对观影体验的影响以及如何选择适合自己设备的视频分辨率。 无论您是电影爱好者还是普通用户,通过本文,您将能够了解到视频分辨率在过去的发展历程和未来的趋势,从而更好地使用和享受视频娱乐。
视频分辨率的历史发展,未来趋势是什么?
|
网络协议 数据安全/隐私保护 网络架构
TCPIP vs OSI模型:网络通信的两种参考模型有哪些不同?
TCP/IP模型和OSI模型都是用于描述计算机网络通信的参考模型,它们在层数和定义、协议和应用层协议、层次结构和数据交换方式等方面都存在不同。

热门文章

最新文章