基于VS调试分析 + 堆栈观察问题代码段

news2024/9/22 19:31:20

在这里插入图片描述

文章目录

  • 问题代码段1 —— 阶乘之和
  • 问题代码段2 —— 越界的危害
    • ① 发现问题
    • ② 分析问题
    • ③ 思考问题【⭐堆栈原理⭐】
    • ④ 解决问题【DeBug与Release】
  • 👨程序员与测试人员👩
  • ✒总结与提炼

问题代码段1 —— 阶乘之和

先来看一道C语言中比较基础的题目,求解阶乘的和,通过调试来观察为何会出现问题,如觉得已经会了的读者可以直接看第二道题

  • 先上代码。逻辑很简答,首先输入n表示,表示n个阶乘之和,然后在内部循环中求出每一个数的阶乘,计算所得进行累加,最后便有了【阶乘之和】
int main()
{
	int sum = 0;	//保存最终结果
	int n = 0;
	int ret = 1;	//保存n的阶乘

	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);

	return 0;
}

简单一些,计算1! + 2! + 3!的阶乘之和。

  • 首先看到我们进入了内存循环,此时i = 1, j = 1,内部的循环只会执行一次,求出1! = 1

在这里插入图片描述

  • 此时的sum便为1

在这里插入图片描述

  • 接着进入【2!】的计算,内部循环会执行2次,此刻i = 2, j = 1

在这里插入图片描述

  • 此时算出ret = 2,即【2! = 2】,累加到sum中,此时sum == 3

在这里插入图片描述

  • 接下去计算【3!】,内部循环会执行3次,3!应该要为6
  • 此刻i = 3, j = 1注意观察此时的ret为2

在这里插入图片描述

  • j == 2时ret进行了一次累乘,值便为4

在这里插入图片描述

  • j == 3时继续进行累乘,此时ret为12

在这里插入图片描述

  • 此时跳出循环开始累加【3!】的结果,此时sum == 15,结果错误❌

在这里插入图片描述

相信在认真看了我步步分析之后,你一定可以清楚为什么会出错❓

  • 原因就在于每次在计算下一个数的阶乘时,上一次累乘之后的ret没有进行一个重置,便导致在计算下一个阶乘的时候重复累乘

在这里插入图片描述

  • 这样运算结果就正确了,1! + 2! + 3! = 9

在这里插入图片描述


你觉得这篇文章真的就那么简单吗?那我就不会写了,上面只是热热身,下面这个才叫做【真正的问题】

问题代码段2 —— 越界的危害

① 发现问题

好,我们来看这个代码段,首先请你给出它最终的运行结果💻

  • 相信很多同学都认为这段代码最后的结果是程序报错,因为一眼就看出了for循环的边界条件有问题,导致产生了数组访问越界
  • 如果你是这么想的,那我要这么告诉你:你是个正常人😀,我问了我身边的朋友,第一时间就觉得这一定是一个越界错误,不过它的运行结果并不是你想的那样,而是一个死循环😵
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;
}

在这里插入图片描述


② 分析问题

看了上面的这些结果,相信很多读者都非常诧异(・∀・(・∀・(・∀・*)。究竟是为什么呢?我们一起先来分析一下🔍

  • 通过调试来进行观察,首先我们进入循环,初始化数组

在这里插入图片描述

  • 然后通过动图先将数组的合法10个元素修改为0,这里的快捷操作是F9设置断点 + F5运行到下一断点处

在这里插入图片描述

  • 但是可以看到,循环的终止条件为i == 12,那此刻循环就会继续执行,那你就会想arr[10]这个位置不是越界非法的嘛,为什么访问不会报错呢?

在这里插入图片描述

  • 首先我们来讨论一下arr[10]这个位置,为什么可以访问到?
  • 从下图可以看出,在内存中对于一个数组而言是连续存储的,数组后面的这些空间其实也是存在arr数组之后,它们都存在于main函数的函数栈帧中,对于函数栈帧来说,一块块存储空间都是紧密相连的,所以要想访问数组之后的空间也是可以访问到,只是在我们的意识中他确实是存在越界访问的行为
  • 可能还是有同学不理解,举个简单的【例子】:假设你现在在一家酒店里,和家里人一起出来旅游,找了酒店中三个连在一起的房间住一晚上。你们旁边呢还有很多房间,都是连在一起的,那这个时候你可以闯入别人的房间🏠吗?虽然行为上是非法的,但是呢又是可以做得到的,只是会被人打一顿而已。这就可以理解为【数组的越界访问】
    在这里插入图片描述
  • 但是越界访问也就算了,难不成真的可以修改没分配内部空间的值吗,我们来看看

在这里插入图片描述

  • 可以看到,这个位置上的值确实是发生了一个修改Σ(っ °Д °;)っ,而且没有报出错误,继续循环。其实对于编译器来说,有时候的越界访问不报出错误是正常的,因为它有时候确实检查不到,就和交警不可能查到每一个醉酒的司机是一个道理,不然为什么有这么多车祸呢🚗
  • 非但是可以修改第10个位置的元素,后面第11个它也进行了修改。虽然这已经见怪不怪了

在这里插入图片描述

  • 可是呢,到了第12个位置的时候,却出现了奇怪的事情,这块地址上的数竟然不是一个随机值,我们都知道面对未初始化的数据都是一个无符号整型(unsigned int),上面也看到过,是一个很大的负数,可是呢这个位置上的数确是12,而且刚好是i == 12这个边界的位置

在这里插入图片描述

  • 此时当我再执行arr[i] = 0时间,就发生了这样的事👇此时的【i】和【arr[12]】两个值竟然同时发生了变化

在这里插入图片描述

  • 那有同学就更加差异了,这是为啥呀?????

在这里插入图片描述

③ 思考问题【⭐堆栈原理⭐】

分析了问题出现的地方,接下去就让我们通过堆栈的内存布局和原理来分析一下为何会出现这样的情况

  • 刚才看到了当程序运行完arr[12] = 0时arr[12]和【i】这两个位置的值一起发生了变化,那就会思考它们会不会是一样的呢?
  • 接着分别在调试的时候取出它们的地址就可以发现确实是同一块空间【我第一次看到这个结果的时候也感觉很惊奇!】

在这里插入图片描述

  • 其实就可以想到,对于变量i,应该是位于数组结束位置的后两位位置,这样才会在越界访问数组的时候导致访问到【i】,然后在修改这块块空间中的值时将循环变量【i】的值做了修改,那也就使得【i】永远到不了13,那也就不会跳出这个循环,会一直循环下去

在这里插入图片描述

  • 变量【i】辛辛苦苦地通过循环加到了13,眼看前面的路就要走完了,但是呢你又给他拽回了起点,那也就只好重新开始。可是呢一次也就算了,你就是和它过不去,就站在13这个位置上,每次看【i】一到13就把他拖回起点,这也就使得它永远都过不去了 —— 血海深仇❣

在这里插入图片描述


当然就通过这样的方式来看还是了解不到在内存中它们究竟发生了什么,就下去我就通过画内存图的方式带你一探究竟🔍

  • 内存布局呢分为【堆区】【栈区】【静态区】三大块,对于像arr数组变量i这些都属于局部变量,对于局部变量来说都是存放在【栈区】中的。也就是我们说过的函数栈帧它就是在栈区开辟空间的
  • 栈也可以称做为【堆栈】,它的使用习惯是:先使用高地址,再使用低地址。这一点很重要,是理解的关键所在!
  • 通过创建两个变量来进行观察就可以发现先创建的变量就会先创建的变量就会现在栈中为其开辟空间,因此可以看到变量a的地址是比变量b的地址来得大的

在这里插入图片描述

  • 下面我画的这张图其实就是内存中栈区的真正模样,也就是从上往下进行生长,上面是高地址,下面是低地址。因此一进到main函数的函数栈帧中时,就会先为变量【i】开辟一块空间,接着可能就会空出几个位置再为arr数组开辟十个元素的空间
  • 可是呢有同学就会疑问,为什么要空出几个位置,而不是直接紧随其后就在变量【i】后面为其分配10块空间呢?这一点我们到后面再议👇
    在这里插入图片描述
  • 我们都知道对于数组的下标来说是从低到高进行变化的,也就是从0 ~ 9,那对于数组的地址是如何变化的呢?我们通过VS来看看

在这里插入图片描述

  • 通过打印数组中每个元素的下标就可以发现数组中每个元素的地址是由低到高进行一个变化的。这一点也很重要,是理解的关键所在!

然后再去看上面这张图你就可以知道为什么在越界访问数组的时候会访问到先创建出来的变量【i】了

👉虽然变量【i】是先创建出来的,先开辟的空间;而数组arr是后创建出来的,后开辟的空间。不过呢,因为数组的下标和每一个元素的地址都是从低到高进行一个变化的又因为堆栈的使用习惯是:先使用高地址,再使用低地址,所以当数组在进行向后访问的时候,就有可能找到变量【i】,就有可能把【i】覆盖掉,就有可能把这个循环变量改成意想不到的值,导致循环的结束条件永远都不会成立,永远都是真,这也就导致了死循环产生👈

你,明白了吗👀


最后的话再来解释一下为什么开辟了变量【i】的栈帧空间后要空出几个位置才为数组arr开辟空间,而且刚好是两个这么巧呢?

  • 其实这不是我瞎说的,也不是我能决定的,而是取决于编译器。
    • VC6.0这个很老的编译器中,其实在局部变量的栈帧空间开辟中是不会再创建多余空间的;
    • gcc这个Linux环境下的编译器中,创建的局部变量之间会空出一个整型,也就是4个字节
    • 但是在VS 2013/2019/2022这些编辑器中,中间都会空出两个整型,也就是8个字节
  • 所以这段代码其实你在不同编译器下去运行虽然都是死循环,但是死循环的临界点和循环的这个范围都是不同的。例如在Linux下的gcc去编译运行的话i = 11就会发生死循环,具体的有兴趣可以去试试

④ 解决问题【DeBug与Release】

好,上面我们通过一系列的问题排查和思索,最终发现了问题所在,那现在就来更正一下这个问题

  • 其实如何更正你已经可以想到了,那就是把对于变量【i】的定义放到数组定义的后面,这样数组在进行越界访问的时候就不会访问到后面的【i】了
  • 不过可以看到,终于是出现了大家一开始想到的越界访问的情况

在这里插入图片描述

  • 不过这种做法可是不对的,数组越界访问应该是我们要避免的一个问题,所以真正要做出修改的应该是循环中访问数组的结束条件

在这里插入图片描述

  • 接下去我们再来看一个神奇的事情,对于代码在编译器中的运行环境我们可以知道有【DeBug】和【Release】两个版本,我将会出现死循环的这中定义方式放在两个不同的斑纹下进行了运行,查看变量i和数组的边界地址
  • 然后便发现【Relsase】版本对变量i的地址做了一个优化,使其变到了数组arr的前面,这样在数组向后进行越界访问的时候就不会发生覆盖造成同时修改了

在这里插入图片描述
在这里插入图片描述

希望在看了我上述对这个问题的讲解之后,今后在碰到类似的问题也可以照常去分析排查🔍


👨程序员与测试人员👩

说一个程序员和测试人员之间的小故事📖

  • 在公司里面,有产品经理,有部门主干,有安全人员,有运维人员,也有程序员和测试人员🐶
  • 但是我们都流传着对于程序员和测试人员是【仇敌】,为什么这么说呢?因为程序员只需要实现当前这段给他的业务逻辑,不需要考虑其他内容,所以他就专注于这块的实现,因此对自己的代码很有信心。可是呢人无完人,每个人都会出错,不然程序员为何要修这么多BUG呢?
  • 当测试人员在他的电脑上运行开发部发来的代码时,却出现了问题,可能是栈溢出、访问异常或者是我们上面说到的访问越界,当她排查了半天发现代码逻辑有问题时,就非常气愤地找到写这块逻辑的程序员👇

在这里插入图片描述
此时就开始了它们的一些争吵。。。。。。。。。此处省略一万字

  • 两个人就这么吵了几十分钟,然后旁边一个资深程序员🐒看不下去了,过来帮忙看了看运行测试了一下发现这块代码逻辑在【DeBug】和【Release】环境下得到的结果是不一样的。
  • 这也就解开了两人之间的矛盾,一对照便知原来测试人员使用的是【Release】环境,而程序员使用的是【DeBug】环境
    • 对于DeBug环境下运行代码,会保留很多的调试信息供程序员测试代码逻辑
    • 对于Release环境下运行代码,会省略很多的调试信息,可能刚好省略了什么重要的内容
  • 所以在工作的时候若是发现和测试人员对口的内容不一致,可以去看看运行环境是否一样

✒总结与提炼

来总结一下本文所学习的内容

  • 首先我们通过一个简单的问题代码段教了大家如何去使用调用排查问题,算是进行了入门
  • 接着通过一段看上去简单但是内部逻辑很是复杂的问题代码段,较大家如何去一步步地排查、分析、思考问题,通过画出堆栈的原理图,找出问题所在,继而解决问题
  • 最后我们通过【程序员与测试人员】之间的故事知道了原来在DeBug和Release环境下运行的代码可能会出现不一样的结果,也多了一个和测试人员解决问题的手段😀

以上便是本文要介绍的所有内容,感谢您的观看,记得给个三连支持哦❤️❤️❤️

在这里插入图片描述

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

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

相关文章

新能源汽车PK燃油汽车,首次胜出,输赢真的那么重要?

新能源汽车PK燃油汽车&#xff0c;输赢真的那么重要&#xff1f;是的。【科技明说 &#xff5c; 每日看点】今天看到一个新能源汽车领域的消息&#xff0c;我觉得很有意思&#xff0c;是说中国新能源汽车满意度水平首次超过了燃油汽车&#xff0c;你们觉得是这样么&#xff1f;…

语义通信:DeepSC用于文本传输也太香了吧

论文标题&#xff1a;Deep Learning Enabled Semantic Communication Systems 论文链接&#xff1a;https://arxiv.org/abs/2006.10685v1 摘要 最近&#xff0c;人们开发了支持深度学习的端到端&#xff08;E2E&#xff09;通信系统&#xff0c;以合并传统通信系统中的所有物…

React中JSX的用法和理解

React的特点 React是用于构建用户界面的高效且灵活的 JavaScript 库&#xff0c;采用组件化模式和声明式编码&#xff1b;使用DOMdiff算法&#xff0c;最大限度地减少与DOM的交互。 相关js库 react.js&#xff1a;React核心库。react-dom.js&#xff1a;提供操作DOM的react扩…

Eclipse导出jar时的错误

文章目录一、发现问题二、解决问题三、新的问题今天&#xff0c;本来是风和日丽&#xff0c;轻风和畅的&#xff0c;复习的一天&#xff0c;直到我开始尝试导出 jar 可执行文件&#xff0c;兄弟们请记住这个词&#xff0c;我将被他折磨很久。一、发现问题 首先&#xff0c;我美…

RT-Thread MSH_CMD_EXPORT分析

RT-Thread MSH_CMD_EXPORT分析 1. 源码分析 在rt-thread中&#xff0c;使用FinSH&#xff0c;可以支持命令行。在源码中&#xff0c;使用MSH_CMD_EXPORT导出函数到对应命令。 extern void rt_show_version(void); long version(void) {rt_show_version();return 0; } MSH_CM…

实战超详细MySQL8离线安装

在RedHat中&#xff0c;RPM Bundle 方式安装MySQL8。建议一定要用 RPM Bndle 版本安装&#xff0c;包全。官网下载&#xff1a;https://dev.mysql.com/downloads/mysql/1.卸载mariadb&#xff0c;会与MySQL安装冲突。rpm -qa | grep mariadb 查看有无mariadb如果有&#xff0…

数据机构笔记哈夫曼编码

1.什么是哈夫曼树&#xff1f;哈夫曼树经典问题&#xff1a;合并果堆问题&#xff1a;如果有三个果堆&#xff0c;其质量分别是1,2,3&#xff0c;我们现在需要将这三堆合并成一堆果堆&#xff0c;合并过程消耗体力等于两堆果堆的质量之和&#xff0c;求最小体力消耗值&#xff…

java贪心算法

1 应用场景-集合覆盖问题 假设存在下面需要付费的广播台&#xff0c;以及广播台信号可以覆盖的地区。 如何选择最少的广播台&#xff0c;让所有的地区 都可以接收到信号 2 贪心算法介绍 贪婪算法(贪心算法)是指在对问题进行求解时&#xff0c;在每一步选择中都采取最好或者最优…

Threadlocal为何引发内存泄漏问题

首先我们要先了解什么是泄漏问题和什么是内存溢出 内存泄漏表示程序员申请了内存&#xff0c;但是该内存一直无法被释放 内存溢出表示申请内存不足&#xff0c;就会报错 为何引发内存泄漏问题 因为每个线程都有自己独立的ThreadLocalMap对象&#xff0c;key为ThreadLocal&…

【C++1】函数重载,类和对象,引用,string类,vector容器,类继承和多态,/socket,进程信号

文章目录1.函数重载&#xff1a;writetofile()&#xff0c;Ctrue和false&#xff0c;C0和非02.类和对象&#xff1a;vprintf2.1 构造函数&#xff1a;对成员变量初始化2.2 析构函数&#xff1a;一个类只有一个&#xff0c;不允许被重载3.引用&#xff1a;C中&取地址&#x…

【shell 编程大全】内容格式化以及多样化输出

内容格式化以及多样化输出 1. 前倾回顾 本章节我们一起来学习下&#xff0c;shell中内容格式化&#xff0c;以及多样输出。但是在学习之前&#xff0c;我们先来看看上个章节【shell 变量的定义以及使用】 我们都学习到了什么知识 shell 变量的定义以及使用 变量分类变量定义类…

SpringBoot设置和读取配置文件(1)

SpringBoot配置文件是用来保存SpringBoot项目当中所有重要的数据的&#xff0c;比如说数据库连接信息&#xff0c;数据库的启动端口&#xff0c;如果端口被占用了&#xff0c;那么就可以随时修改&#xff1b; 1)比如说我们之前再写JDBC的代码的时候&#xff0c;要去写链接字符串…

C 字符串

在 C 语言中&#xff0c;字符串实际上是使用空字符 \0 结尾的一维字符数组。因此&#xff0c;\0 是用于标记字符串的结束。空字符&#xff08;Null character&#xff09;又称结束符&#xff0c;缩写 NUL&#xff0c;是一个数值为 0 的控制字符&#xff0c;\0 是转义字符&#…

SNI生效条件 - 补充nginx-host绕过实例复现中SNI绕过的先决条件

文章目录1.前置环境搭建2.测试SNI生效条件(时间)3. 证书对SNI的影响3.1 双方使用同一个证书&#xff1a;3.2 双方使用不同的证书与私钥4. 端口号区分测试4.1 端口号区分&#xff0c;证书区分&#xff1a;4.2 端口号区分,证书不区分&#xff1a;5.总结SNI运行机制6. SNI机制绕过…

Docker-安装Jenkins-使用jenkins发版Java项目

文章目录0.前言环境背景1.操作流程1.1前期准备工作1.1.1环境变量的配置1.2使用流水线的方式进行发版1.2.1新建流水线任务1.2.2流水线操作工具tools步骤stages步骤1:拉取代码编译步骤2:发送文件并启动0.前言 学海无涯&#xff0c;旅“途”漫漫&#xff0c;“途”中小记&#xff…

从0到1一步一步玩转openEuler--12 openEuler用户管理

文章目录12.1 创建用户12.1.1 useradd命令12.1.2 用户信息文件12.1.3 创建用户实例12.2 修改用户账号12.2.1 修改密码12.2.2 修改用户shell设置12.2.3 修改主目录12.2.4 修改UID12.2.5 修改账号的有效期12.3 删除用户12.4 管理员账户授权在Linux中&#xff0c;每个普通用户都有…

【Java 面试合集】怎么声明一个类不会被继承,以及应用场景

怎么声明一个类不会被继承&#xff0c;以及应用场景1. 概述 今天的Java 面试合集又来了。今天我们复习的问题是:怎么声明一个类&#xff0c;不可以被继承 2. 验证 public final class TestMath { }通过上述截图 我们可以看到&#xff0c;被关键字final 修饰过的类&#xff0c;…

EOC第六章《块与中枢派发》

文章目录第37条&#xff1a;理解block这一概念第38条&#xff1a;为常用的块类型创建typedef第39条&#xff1a;用handler块降低代码分散程度第41条&#xff1a;多用派发队列&#xff0c;少用同步锁方案一&#xff1a;使用串行同步队列来将读写操作都安排到同一个队列里&#x…

02 OpenCV图像通道处理

1 通道提取与合并 在数字图像处理中&#xff0c;图像通道是指一个图像中的颜色信息被分离为不同的颜色分量。常见的图像通道包括RGB通道、灰度通道、HSV通道等。 RGB通道是指将图像分离为红色、绿色和蓝色三个颜色通道&#xff0c;每个通道表示相应颜色的亮度。这种方式是最常…

【QT 5 相关实验-仪表盘-学习笔记-表盘组件练习与使用总结】

【QT 5 相关实验-仪表盘-学习笔记-表盘组件练习与使用总结】1、概述2、实验环境3、参考资料-致谢4、自我提升实验效果5、代码练习-学习后拆解&#xff08;1&#xff09;头文件部分&#xff08;2&#xff09;绘制事件绘制表盘代码&#xff08;3) 每一块部分绘制6、代码移植提升类…