【调试方法】基于vs环境下的实用调试技巧

news2024/10/3 4:38:30

前言:

对万千程序猿来说,在这个世界上如果有比写程序更痛苦的事情,那一定是亲手找出自己编写的程序中的bug(漏洞)。作为新手在我们日常写代码中,经常会出现报错的情况(好的程序员只是比我们见过的bug多从而减少出错),但当我们遇到报错时大家可能都会出现看不懂的情况,以至于在那里捣鼓半天最后还是当上了“C/V”工程师。本期,基于vs环境下我将带领大家去搞懂代码的调试的小技巧。

在这里插入图片描述

文章目录

  • 1. 什么是bug?
  • 2. 调试是什么?有多重要?
    • 2.1 调试是什么?
    • 2.2 调试的基本步骤
    • 2.3 Debug和Release的介绍
  • 3. Windows环境调试介绍
    • 3.1 调试环境的准备
    • 3.2 学会快捷键
  • 4.实例演示
    • 4.1实例一:阶乘之和
    • 5.2 实例二:死循环问题

1. 什么是bug?

首先,当我们想要去战胜它时,我们先要了解它。就像打战一样,知己知彼方能百战不殆。

大概由来就是有一次在编写程序计算机发生故障,经过排查,在计算机的继电器触电里,找到了一只被夹扁的小飞蛾,这只小虫子卡住了机器的运行,并诙谐的把程序故障称为“bug”。这就是我们今天最爱说的“bug”的由来。它的意思,和原身一致,真就是“一只臭虫”。
在这里插入图片描述
具体原因可以了解:
bug的由来


2. 调试是什么?有多重要?

就像警察办案,根据线索一步步的推理和考察,最后得出最后的真相。或许我们最有印象的就是我们看过的【名侦探柯南】。一名优秀的程序员是一名出色的侦探,每一次调试都是尝试破案的过程*

对于绝大多数的新手玩家而言,我们写代码就是“三下五除二”,管它三七二十一一上来就是一顿猛敲,但是到最后的看着密密麻麻的报错,人都要麻了。
在这里插入图片描述
又是如何排查出现的问题的呢?
在这里插入图片描述
调试错误时或许像这样:

在这里插入图片描述
这样无脑的去进行增删查改,到最后忙活半天可能都还在原地踏步。因此,掌握好调试就显得十分重要。


2.1 调试是什么?

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

2.2 调试的基本步骤

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


2.3 Debug和Release的介绍

紧接着我们再来看一下VS下的两种版本,即-----Debug和Release

a:
Debug通常称为调试版本,通过一系列编译选项的配合,编译的结果通常包含调试信息,而且不做任何优化,以为开发人员提供强大的应用程序调试能力,便于程序员调试程序。
b:
而Release通常称为发布版本,是为用户使用的,一般客户不允许在发布版本上进行调试。所以不保存调试信 息,同时,它往往进行了各种优化,以期达到代码最小和速度最优。为用户的使用提供便利。

我们还是通过代码来展示:

#include<stdio.h>

int main()
{
	char* p = "hello world";
	printf("%s\n", p);

	return 0;
}

当我们写出以上代码并且放在【Debug】版本下运行
在这里插入图片描述

当我们去文件查看在【debug】下的信息时,我们看到结果如下图所示:
在这里插入图片描述

而当我们的代码在【Release】版本下运行的时候:

在这里插入图片描述

我们可以看到同样的程序在两个版本下的文件大小是不同的。
在这里插入图片描述

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

那编译器进行了哪些优化呢?
请看如下代码:

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 】模式去编译,程序没有死循环。

在这里插入图片描述

那他们之间有区别,就是因为优化导致的。


3. Windows环境调试介绍

注:linux开发环境调试工具是gdb,后期课程会介绍

3.1 调试环境的准备

在这里插入图片描述
在环境中选择 debug 选项,才能使代码正常调试。

3.2 学会快捷键

在这里插入图片描述

上图我打钩的就是平时经常用得到的一些快捷键,记住快捷键将大大提高我们调试的效率,接下来我将具体介绍:

F5

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

F9

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

F10

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

F11

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

CTRL + F5

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

更多的快捷键,可以通过如下查看:
https://blog.csdn.net/mrlisky/article/details/72622009


4.实例演示

说了那么多,终究全是书面上的东西,接下来我们通过具体的实例带大家感受一下。

4.1实例一:阶乘之和

代码思维:

在我们开始写代码之前一定要想好n的阶乘和是什么?
当我们脑海中有了思路之后,写起来就会很快,而不是直接就上手:逻辑很简答,首先输入n表示n的阶乘之和,最后在进行求和操作即可

紧接着,想每一步需要用到的知识,具体如下:

1、阶乘:1x2x3…xn 用到了循环语句

2、求和:还是用到了循环

最后就是打印出来即可!!

代码如下:

int main()
{
	int i = 0;
	int sum = 0;//保存最终结果
	int n = 0;
	int ret = 1;//保存n的阶乘(乘法的话一定要初始化为1)
	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】为例。在我们的想象中,【3】的阶乘之后就是:

第一步:1的阶乘,即1;
第二步:2的阶乘,即12=2;
第三步:3的阶乘,即1
2*3=6;
第四步:三数相加,即1+2+6=9(即最终结果为9)

那么真的是这样吗?接下来我们就通过一步步的调试去看看结果。

首先看到我们进入了内存循环,此时i = 1, j = 0,循环会开始执行一次,因此可以求出此时【1】的阶乘为 :1! = 1

在这里插入图片描述
当第一次跳出内层循环之后,我们就可以求出【1!】,因此可以看到【sum】此时为1

在这里插入图片描述
紧接着我们再去计算【2!】,此时在我们函数的内部,会进行两次的循环操作,执行完毕后,此时各个值如下表所示:

在这里插入图片描述
此时将我们【2!】计算完毕之后,可以得到【ret】的值,最后在进行累加的操作,即【1+2】的操作,所以此时的【sum】应该为3,结果如下:

在这里插入图片描述
前两次执行完之后,紧接着就回去执行【3!】的操作,在内部进行三次循环操作,我们已经知道,【3!】的结果为6,我们在继续【F10】看结果:

第一次内部循环开始,此时【j=1】,【ret=2】

在这里插入图片描述

第二次循环完毕之后,此时【j=2】,【ret=4】

在这里插入图片描述

第三次循环完毕之后,我们可以发现此时【j=3】,【ret=12】

在这里插入图片描述

最后一步在循环完之后,我们就需要计算累加和,此时我们可以发现当【j=4】时我们跳出循环操作,而最终的结果显示的是【15】,明明应该是【9】啊.(咦…怎么会是【15】呢?此时各位好奇的小脑袋瓜就开始躁动起来了)

在这里插入图片描述

遇到程序出错了不要害怕,我们仔细分析一下。 我们来整理一下,这样写的思路:

a:
n=1时,我们进入第一个循环,然后并没有发生什么,进入第二个循环【ret=1*1】,【sum=0+1】,此时是没有问题的;
b:
n=2时,我们在进入在第一个循环,没有发生什么,进入第二个循环,【ret=1 * 1=1】,但是请注意,在这之后,并不会计算【sum=sum+ret】,而是继续在第二个循环中没有跳出来,因为第二个循环的条件是【i<2】,此时仍然是真的,所以,第二个循环继续,【ret=1 *2=2】;在跳出第二个循环,【sum=0+2】,但是请注意,此时第一个循环没有结束,对于第一个循环,此时【n=1】,还要继续【n=2】的情况,所以最终结果是【4】,因此在这一步就出现了错误。

因此我们可以这样改(每次重置【ret】的值),此时运行结果就正确了:
在这里插入图片描述
还有一种方法就是我们可以不使用两层嵌套来进行封装,我们只定义一层循环,具体代码如下:


int main()
{
	
	int n = 1;
	scanf("%d", &n);

	int ret = 1;//保存n的阶乘(乘法的话一定要初始化为1)
	int i = 1;
	int sum = 0;//保存最终结果

	for (i = 1; i <= n; i++)
	{
		ret *= i;
		sum += ret;
	}

	printf("%d\n", sum);
	return 0;
}

5.2 实例二:死循环问题

首先我给出一段代码,大家可以猜猜程序最后将会输出什么:

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

相信大多数的小伙伴看到这个程序的第一印象就是,数组下标为【0-9】,而这里却是【<=12】,很显然的问题就是数组访问越界

然而真的是这样的吗?老规矩直接运行程序,看最终是不是我们想的那样。

在这里插入图片描述

咦…我们发现怎么结果会是死循环打印呢?接下来,你如果要搞清楚这个问题,你能肉眼分析出来是因为什么吗?这时候就需要用到调试了

开始时我们进入循环,对数组进行初始化操作,得到以下结果
在这里插入图片描述
紧接着我们进入循环里面去,对数组元素都改为【0】,一直进行到对【arr[9]】的操作,到这个元素都是在我们的正常范围之内进行的。

在这里插入图片描述

那么接下来的【arr[10]】呢?【arr[11]】和【arr[12]】呢?它们是什么样的呢?我们继续调试下去
在这里插入图片描述

从上图 我们可以发现它依然对其进行操作,那么为什么对【arr[10]】这个位置还能访问得到呢?接下来我给大家解释解释

在这里插入图片描述
因为当我们的数组存,放在内存中的时候是有一片连续存储的空间,而数组之后也存在一定的空间,而这一片连续的地址空间都存在于【main】函数的栈帧空间下,在它看来存储空间之间都是连续的,因此数组之后的空间也是可以访问到的。

除了这个问题之外,还有一个明显的问题不知道大家有没有发现,就是当我们执行到最后时,【i】和【arr[12]】两个值竟然同时变为了0,这又是为什么呢?
在这里插入图片描述
接下来我们我们分别对两个进行取地址的操作,就可以发现这两个竟然是指向的同一片地址空间
在这里插入图片描述

到了这我们就可以想到,对于变量【i】,它应该是位于整个数组结束位置的后两位上,只有这样才会在数组越界访问的情况下改变【i】的值,最后在修改这块块空间中的值时将循环变量【i】的值做了修改,因此使得【i】的值永远不可能到达13,因此才会出现死循环打印的情况。

接下来,我们通过内存布局来进行进一步的了解。

首先我们知道【i】和【arr】是局部变量元素,而局部变量是放在内存中的栈区上的,而栈区的使用习惯是先用高地址空间再用低地址空间(这点非常重要),可以发现变量a的地址是比变量b的地址来得大的。
在这里插入图片描述

那么在内存中栈区究竟是怎么样的呢?我们通过以下图片为例。
在这里插入图片描述
程序一进到【main】函数的函数栈帧中时,就会先为变量【i】开辟一块空间,接着可能就会空出几个位置再为【arr】数组开辟十个元素的空间,根据上述我们可以发现空出来了几个位置,那么为什么要空出来呢?(这里面可是有大学问的)

这并不是我规定的或者谁规定的,中间的大小而是取决于编译器
1.VC6.0编译器下,中间就没有多余的空间;
2.在gcc这个Linux环境下的编译器中,创建的局部变量之间会空出一个整型,也就是4个字节
3.在VS 2013/2019/2022这些编辑器中,中间都会空出两个整型,也就是8个字节

因此在不同的编译器下去运行这段代码虽然得到的都是死循环这个现象,但是底层的实现是有些区别的。

紧接着我们可以知道数组在平时的使用过程中都是从低到高的,但是数组的地址是否也是这样的呢?我们进行一下测试。

在这里插入图片描述

从上图我们可以发现数组的每个元素地址都是从低到高进行一个变化的。

有了这些知识储备之后,我们在回过头去看最开始的问题,这时就可以很好的回答了:

程序开始的时候,变量【i】先创建出来,在内存中先开辟的地址空间;而【arr】数组的地址空间是后开辟出来的。但是,刚才我们已经知道数组的下标和数组元素的地址变化顺序都是从低到高,而内存中的堆栈则是先使用高地址,再使用低地址,因此当数组进行向后访问时,就有可能找到变量【i】,并且把其覆盖掉,因此就有可能把循环变量的值改为其他的情况,从而导致循环结束条件不能达到,就导致了死循环打印的现象。(到此一切就讲通了)

因此,正确的解决方案还是改我们的循环结束条件。


总结:

通过本期的学习,我相信大家以后在遇到程序出现报错的时候就不会无脑的直接去程序里面增删查改了,大家可能会说这个很难,但是俗话说得好呀!(害怕恐惧的最好办法就是战胜恐惧)

以上便是本期的所有内容啦!感谢您的观看,如果对你有帮助的话记得三连支持一下哟!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/380128.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

4.排序算法之一:冒泡排序

排序算法稳定性假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序&#xff0c;这些记录的相对次序保持不变&#xff0c;即在原序列中&#xff0c;r[i]r[j]&#xff0c;且r[i]在r[j]之前&#xff0c;而在排序后的序列中&#xff0c;r[…

操作系统权限提升(二十一)之Linux提权-环境变量劫持提权

系列文章 操作系统权限提升(十八)之Linux提权-内核提权 操作系统权限提升(十九)之Linux提权-SUID提权 操作系统权限提升(二十)之Linux提权-计划任务提权 环境变量劫持提权 环境变量劫持提权原理 PATH是Linux和类Unix操作系统中的环境变量&#xff0c;类似windows中的path环…

蓝海彤翔执行副总裁张加廷接受【联播苏州】独家专访

今年春节档&#xff0c;科幻类电影《流浪地球2》票房口碑双丰收&#xff0c;截至目前&#xff0c;累计票房已破 38 亿&#xff0c;淘票票评分 9.6 &#xff0c;影片的特效质感可以媲美国际顶尖水平。其中&#xff0c;蓝海彤翔为影片的后期制作提供了出色的渲染服务。2月21日&am…

前端学习第二阶段-第4章 移动web开发

4-1 媒体查询 01-移动WEB开发rem适配布局导读 02-rem单位 03-媒体查询语法简介 04-媒体查询案例背景变色 05-媒体查询rem实现元素动态大小变化 06-媒体查询引入资源 4-2 less介绍和使用 07-CSS的弊端 08-less简介以及安装 09-less变量 10-less编译easy less插件 11-less嵌套 12…

Linux命令-mdadm管理磁盘阵列组

文章目录​​​​​​​ 概要 一 磁盘阵列是什么&#xff1f; 二 RAID的级别 RAID 0 RAID 1 RAID 5 RAID10 三 命令介绍 四 语法格式 五 基本参数 六 参考实例 创建RAID 0磁盘阵列组 创建RAID 1磁盘阵列组 创建RAID 5磁盘阵列组 创建RAID 10磁盘阵列组…

【Flutter入门到进阶】Flutter基础篇---第一个Flutter应用

1 Flutter目录结构介绍 1.1 创建项目 flutter create flutterdemo 1.2 目录结构 1.3 结构说明 1、android、ios、linux、macos、web、windows文件夹&#xff1a;都是对应平台相关代码 2、lib文件夹&#xff1a;flutter相关代码&#xff0c;我们编写的代码就在这个文件夹 3、t…

八、异步编程

文章目录异步编程FutureTask应用&源码分析FutureTask介绍FutureTask应用FutureTask源码分析FutureTask中的核心属性FutureTask的run方法FutureTask的set&setException方法FutureTask的cancel方法FutureTask的get方法FutureTask的finishCompletion方法CompletableFuture…

DevOps 学习笔记(一) | DevOps 简介及环境搭建

1. 环境配置 本次实验需要三台服务器CI/CD 服务器、应用服务器和Harbor 服务器 DevOps 步骤 程序员将代码 push 到代码仓库Jenkins 根据触发条件拉取代码到CI/CD 服务器Jenkins 使用 Maven 将代码 build 成 jar 包Jenkins 使用 jar 包通过 Dockerfile 和 docker-compose.yml…

HBase JMX 指标学习

名词解释&#xff1a; JMX&#xff1a;Java Management Extensions&#xff0c;用于用于Java程序扩展监控和管理项。 GC&#xff1a;Garbage Collection&#xff0c;垃圾收集&#xff0c;垃圾回收机制。 1、概述 说到对Hadoop和 HBase的集群监控&#xff0c;大家知道的和用…

YOLOv8详解 【网络结构+代码+实操】

文章目录YOLOv8 概述模型结构Loss 计算训练数据增强训练策略模型推理过程网络模型解析卷积神经单元&#xff08;model.py&#xff09;Yolov8实操快速入门环境配置数据集准备模型的训练/验证/预测/导出使用CLI使用python多任务支持检测实例分割分类配置设置操作类型训练预测验证…

FastDDS-4.RTPS层

4. RTPS层 eprosima Fast DDS的较低层RTPS层是RTPS标准协议的实现。与DDS层相比&#xff0c;该层提供了对通信协议内部的更多控制&#xff0c;因此高级用户可以更好地控制库的功能。 4.1 与DDS层的关系 该层的元素与DDS层的元素一一对应&#xff0c;并添加了一些元素。该对应…

【使用两个栈实现队列】

文章目录一、栈和队列的基本特点二、基本接口函数的实现1.栈的接口2.创建队列骨架3.入队操作4.取出队列元素5.返回队首元素6.判断队列是否为空7.销毁队列总结一、栈和队列的基本特点 栈的特点是后进先出&#xff0c;而队列的特点是先进先出。 使用两个栈实现队列&#xff0c;必…

【DataX】数据同步到PG时遇到的分区不存在问题

数据同步到PG时遇到的分区不存在问题前言正文问题分析解决方法结语前言 大概说下这个问题牵扯出来的背景&#xff0c;一个外场项目&#xff0c;选型用PG存业务数据&#xff0c;然后客户要求保存保留一年的数据&#xff0c;运行到现在服务器5个T的磁盘已经有点扛不住了&#xf…

内存的管理

取指令——译码——执行——返存 计组课我们学过cpu真正读指令并非是从内存中读入&#xff0c;而是从cache读和存&#xff0c;再由cache进行取指或返存&#xff0c;因为cpu指令周期比内存周期速度快很多&#xff0c;cpu若要取指或返存都需要等待内存完成他的动作才可以进行下一…

python爬虫:如何定义内容提取器

项目背景 在python 即时网络爬虫项目启动说明中我们讨论一个数字&#xff1a;程序员浪费在调测内容提取规则上的时间&#xff0c;从而我们发起了这个项目&#xff0c;把程序员从繁琐的调测规则中解放出来&#xff0c;投入到更高端的数据处理工作中。 解决方案 为了解决这个问题…

微信小程序使用scss编译wxss文件的配置步骤

文章目录1、在 vscode 中搜索 easysass 插件并安装2、在微信开发工具中导入安装的easysass插件3、修改 spook.easysass-0.0.6/package.json 文件中的配置4、重启开发者工具&#xff0c;就可用使用了微信小程序开发者工具集成了 vscode 编辑器&#xff0c;可以使用 vscode 中众多…

C++修炼之练气期三层——函数重载

目录 1.引例 2.函数重载的概念 3.C支持函数重载的原理 1.引例 倘若现在要实现一个加法计算器&#xff0c;用C语言实现的话我们会选择这样的方式&#xff1a; int Add_int(int a, int b) {return a b; }double Add_double(double a, double b) {return a b; } 在使用加…

Exposure2023专业摄影RAW格式大师专业滤镜特效

Exposure2023是一款专为摄影艺术设计的图像编辑器。新的 Exposure2023结合了专业级的照片调整、庞大的华丽照片库和令人愉悦的高效设计。可以提供最大&#xff0c;最准确的电影外观选择。Exposure的创意外观不仅限于电影模拟&#xff0c;从干净优雅的现代风格到引人注目的色彩变…

SpringBoot+Nacos+OpenFeign环境搭建

目录 1.boot方式nacos与openFeign集成 1.引入依赖 2.添加配置 3.测试接口调用 4.常见问题&#xff1a; 1.版本依赖 2.nacos客户端 2.cloud方式nacos与openFeign集成 1.引入依赖 2.添加配置 3.接口定义 4.开启FeignClients客户端 5.远程接口测试 6.Nacos配置中心 1…

Java - 数据结构,二叉树

一、什么是树 概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。它具有以下的特点&#xff1a; 1、有…