| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> C陷阱:数组越界遍历,不报错却出现死循环?从内存解析角度看数组与局部变量之“爱恨纠葛” -> 正文阅读 |
|
[C++知识库]C陷阱:数组越界遍历,不报错却出现死循环?从内存解析角度看数组与局部变量之“爱恨纠葛” |
在平时的代码练习中,数组越界访问当然是会被规避的。然而,如果运行了令数组越界访问的代码,会产生什么后果?如果我们从未了解过,可能会下意识地认为,编译器会报错、阻止程序运行,或直接挂掉程序。 事实上还有一种较为常见的结果:死循环。循环遍历数组时,如果遍历的数组下标超出数组长度,程序无休止地卡在了循环体内。这和栈中数组与循环变量(局部变量)创建的位置紧密相关。 本文从创建数组和局部变量的内存解析角度,对上述情况加以说明。学习C语言的我们也应当对这类情况有所了解并能清晰解释。 本文先用调试和画图的方式解析死循环出现的原因,文末附上了完整的文字解释,可供大家参考。 注:?本文实验采用的编译器为 RedPanda DevC++,在较新版本的编译器(如VS 2022)下测试结果可能会有差异,因为新的编译器会对代码进行优化。以下是本文的主要内容。 我们来看下面这组代码:
上述程序代码中,数组长度为10,然而下标却取到了11、12,数组的下标越界。 当程序运行后,出现了如下情况: 注意:编译器并未对下标越界的情况报错,程序可以正常启动,但在程序中出现了bug。光以肉眼看,我们无法排查出问题。 我们通过调试来进一步观察。? 目录 一、调试?1. 启动调试,我们在左侧添加一些监视,包括循环变量i与arr[9](最后一个数组元素)到arr[12]之间的值。 此时理论上来说,arr[10]、arr[11]、arr[12]三个数由于不是我们的数组元素,没有被初始化,里面装的是随机值。 2. 单步调试。在第二次结束循环体后可以看到,i 的值为2,数组的前两个元素arr[0]与arr[1]也如期被我们改成了0.控制台也打印了两个"hello",此时程序的运行一切正常。 ? ? 3. 经过观察我们发现,当 i 从0到9时(也就是在数组界内时),程序的运行都是正常的:每次循环都有一个数组元素被置0,并且打印一个"hello"。我们让i快速来到9,这时我们观察在这之后程序内各变量值的变化。 ? 4. 我们让 i 来到10,这时有:数组arr[10],要操作arr[10] = 0;注意,这里也并不会有任何报错提示。因为数组名本质上是一个地址,arr[10]即*(arr+10),尽管此时的空间并不是我们数组内的合法空间,但依旧能够访问并更改其值。 如下图,arr[10]也确实被改成了0. (内存中不属于数组元素的值竟然也能被直接更改,可见野指针/数组越界访问确实是存在非常大的风险的。) 这时重点来了!我们再向下走,当arr[11]也被改成0时,i竟然也被改成了0!! 很难让人不怀疑:有没有可能,arr[11]和i共用的是同一个空间?换句话说,arr[11]就是i? 通过取地址来求证,我们发现,arr[11]与i的地址相同,从而印证了我们上面的猜想:arr[11]实际上就是i 事实上如果一开始就在监视窗口中添加了arr[11],可以很清晰地发现,随着每次循环i的值发生变化,arr[11]的值也一直在变化:? ? 确实,arr[11]与i是同步变化的,arr[11]就是i,它们共用同一块内存单元,它们实质上代表同一个变量。? 因此,每当 i 到达11时候,arr[11]就会被置为0,而arr[11]就是i ,相当于 i 被置为0.于是,?i 在0到11之间反复横跳,永远也无法跳出这个范围。同样,arr[12]永远也无法遍历到,循环无法自行结束,这样一来便出现了死循环的现象。 下面我们画内存解析图,分析底层原理。 二、内存解析图解首先我们需要知道:
?上述代码中,创建局部变量i和数组的代码顺序是这样的:
由栈区内存的使用习惯知,变量 i?的地址要高于arr数组。因此,内存中各个变量的创建位置如下图所示: 绿色部分为数组元素,连续的10个空间;蓝色部分为arr[11],即 i 。? 当遍历数组元素时,自低地址向高地址走,而 i 又恰好“等”在高地址的某处。数组不越界则已,一越界就很有可能碰上在“高”处等候的变量 i 。此时无意间操作了循环变量 i ,就极有可能造成循环失控,而死循环正是循环失控的一种。 ?同样,这种循环失控并不是百分百的。就拿本题来说,若把 i 的边界控制成为 i <10,令 i 到不了11,那么程序依旧可以正常运行,不会报错或失控。 那么变量 i 的空间与数组末元素的空间相隔多少个空间呢? 这完全取决于编译器如何实现。经过不完全测验,一些编译器及其空间分配关系如下:
release模式下,代码会自动优化,规避掉可能存在的隐患。因此在release模式下运行,也不会出现循环失控。 ? 如果调换代码书写的位置,又会如何呢?
这时,由于变量创建的顺序发生了变化,而内存的使用习惯仍然是先使用高地址,再使用低地址,此时我们的变量 i 在内存中的地址在数组arr的更低地址处,此时不会发生循环失控。 ? 需要注意的是,虽然这样不会发生循环失控,但仍然可能会有新的问题。毕竟数组发生越界,且强行更改了不属于数组元素空间内的值,编译器可能会弹窗报错(类似上面vs2022的处理情况)。 之所以原来的代码书写位置不会让编译器报错,是因为程序卡在了死循环,没有报错的机会。而当不出现死循环时,编译器就有可能发现异常,弹窗报错。(具体看编译器的处理。) 三、总结:原因解释面试题解释变量i与数组a在栈上开辟空间,而我们知道,栈区的使用习惯是先使用高地址再使用低地址,因此i在高地址的位置,arr数组在低地址的位置。同时,随着数组下标的增长,地址是由低到高变化的。数组适当往后越界,就有可能覆盖到 i ,将循环变量 i 改变,从而导致循环的判断条件恒为真,最终造成程序循环失控。(结合示意图说明更清晰。) |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 12:57:26- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |