一。c++在内存区域的分配图

可以看出,对于Linux系统下的,存储空间的分配有着较为层次清晰的分层。单片机大概也遵循这个分区架构。

二进制代码以及常量(CONST修饰)以及全局变量在最底层,存储空间最靠前的部分
然后是堆区,堆区向上增长,我们常用到的molloc()、free()等函数操作的就是这个区,这也是芯片系统中唯一可以让程序员通过代码操作的一片存储空间
再然后是动态链接库
在往上(高地址)便是栈区。 最高地址一般为操作系统内核,用户无法访问

二。什么是栈,栈溢出是什么怎么解决

在计算机中,栈可以理解为一个特殊的容器,用户可以将数据依次放入栈中,然后再将数据按照相反的顺序从栈中取出。也就是说,先放入的数据最后才能取出,而最后放入的数据必须先取出。这称为先进后出(First In Last Out)原则。

放入数据常称为入栈或压栈(Push),取出数据常称为出栈或弹出(Pop)。

可以发现,栈底始终不动,出栈入栈只是在移动栈顶,当栈中没有数据时,栈顶和栈底重合。

这里需要注意标识栈顶和栈底的两个寄存器: ebp寄存器指向栈底,esp寄存器指向栈顶。从本质上来讲,栈是一段连续的内存,需要同时记录栈底和栈顶,才能对当前的栈进行定位。

栈溢出是怎么回事

了解了栈实际上也是一块内存后,栈溢出就好理解了。

  因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。

当我们定义的数据所需要占用的内存超过了栈的大小时,就会发生栈溢出。编译器会报栈溢出错误。

如一块芯片的内存RAM大小为4k,当我们定义了一个大数组,如下:

int buf[1024*5] = {0};
  • 1

很明显定义的数组超过了内存大小,这就导致了栈溢出。

解决方案:

方法一:用栈把递归转换成非递归

通常,一个函数在调用另一个函数之前,要作如下的事情:a)将实在参数,返回地址等信息传递给被调用函数保存; b)为被调用函数的局部变量分配存储区;c)将控制转移到被调函数的入口. 从被调用函数返回调用函数之前,也要做三件事情:a)保存被调函数的计算结果;b)释放被调函数的数据区;c)依照被调函数保存的返回地址将控制转移到调用函数.所有的这些,不论是变量还是地址,本质上来说都是"数据",都是保存在系统所分配的栈中的. 那么自己就可以写一个栈来存储必要的数据,以减少系统负担。

方法二:使用static对象替代nonstatic局部对象

在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。由上图可知全局变量区内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在

方法三:增大堆栈大小值

当创建一个线程的堆栈时,系统将会保留一个链接程序的/STACK开关指明的地址空间区域。但是,当调用CreateThread或_beginthreadex函数时,可以重载原先提交的内存数量。这两个函数都有一个参数,可以用来重载原先提交给堆栈的地址空间的内存数量。如果设定这个参数为0,那么系统将使用/STACK开关指明的已提交的堆栈大小值。后面将假定我们使用默认的堆栈大小值,即1MB的保留区域,每次提交一个页面的内存。

函数调用栈

定义的数组会占用栈空间,同样,定义的函数也会占用栈空间,一个简单的例子便是函数的入栈和出栈。

举个例子:

void func(int a, int b)
{
int p =12, q = 345;
}
int main()
{
func(90, 26);
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

函数的进栈出栈过程如下图所示:

函数进栈

1) main() 是主函数,也需要进栈,如步骤①所示。

2) 在步骤②中,执行语句func(90, 26);,先将实参 90、26 压入栈中,再将返回地址压入栈中,这些工作都由 main() 函数(调用方)完成。这个时候 ebp 的值并没有变,仅仅是改变 esp 的指向。

3) 到了步骤③,就开始执行 func() 的函数体了。首先将原来 ebp 寄存器的值压入栈中(也即图中的 old ebp),并将 esp 的值赋给 ebp,这样 ebp 就从 main() 函数的栈底指向了 func() 函数的栈底,完成了函数栈的切换。由于此时 esp 和ebp 的值相等,所以它们也就指向了同一个位置。

4) 为局部变量、返回值等预留足够的内存,如步骤④所示。由于栈内存在函数调用之前就已经分配好了,所以这里并不是真的分配内存,而是将 esp 的值减去一个整数,例如 esp - 0XC0,就是预留 0XC0 字节的内存。

5) 将 ebp、esi、edi 寄存器的值依次压入栈中。

6) 将局部变量的值放入预留好的内存中。

至此,func() 函数的活动记录就构造完成了。可以发现,在函数的实际调用过程中,形参是不存在的,不会占用内存空间,内存中只有实参,而且是在执行函数体代码之前、由调用方压入栈中的。

未初始化的局部变量的值为什么是垃圾值

为局部变量分配内存时,仅仅是将 esp 的值减去一个整数,预留出足够的空白内存,不同的编译器在不同的模式下会对这片空白内存进行不同的处理,可能会初始化为一个固定的值,也可能不进行初始化。

函数出栈

步骤⑦到⑨是函数 func() 出栈过程:
7) 函数 func() 执行完成后开始出栈,首先将 edi、esi、ebx 寄存器的值出栈。

8) 将局部变量、返回值等数据出栈时,直接将 ebp 的值赋给 esp,这样 ebp 和 esp 就指向了同一个位置。

9) 接下来将 old ebp 出栈,并赋值给现在的 ebp,此时 ebp 就指向了 func() 调用之前的位置,即 main() 活动记录的 old ebp 位置,如步骤⑨所示。

这一步很关键,保证了还原到函数调用之前的情况,这也是每次调用函数时都必须将 old ebp 压入栈中的原因。

最后根据返回地址找到下一条指令的位置,并将返回地址和实参都出栈,此时 esp 就指向了 main() 活动记录的栈顶, 这意味着 func() 完全出栈了,栈被还原到了 func() 被调用之前的情况。

函数执行完局部变量的值真的不存在了?

经过上面的分析可以发现,函数出栈只是在增加 esp 寄存器的值,使它指向上一个数据,并没有销毁之前的数据。

栈上的数据只有在后续函数继续入栈时才能被覆盖掉,这就意味着,只要时机合适,在函数外部依然能够取得局部变量的值。请看下面的代码:

#include <stdio.h>
int *p;
void func(int m, int n)
{
int a = 18, b = 100;
p = &a;
}
int main()
{
int n;
func(10, 20);
n = *p;
printf("n = %d\n", n);
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

运行结果:

n = 18

在 func() 中,将局部变量 a 的地址赋给 p,在 main() 函数中调用 func(),函数刚刚调用结束,还没有其他函数入栈,局部变量 a 所在的内存没有被覆盖掉,所以通过语句n = *p;能够取得它的值。

c++内存区域结构及堆栈的一些知识的更多相关文章

  1. JVM内存区域的划分(内存结构或者内存模型)

    JVM内存区域的划分(内存结构或者内存模型)   运行时数据区域: 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 程序计数器(线程私有): 是当前线程所 ...

  2. C/C++中内存区域划分大总结

    C++作为一款C语言的升级版本,具有非常强大的功能.它不但能够支持各种程序设计风格,而且还具有C语言的所有功能.我们在这里为大家介绍的是其中一个比较重要的内容,C和C++内存区域的划分. 一. 在c中 ...

  3. 深入理解java虚拟机系列(一):java内存区域与内存溢出异常

    文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了開始.假设有什么错误或者遗漏,欢迎指出. 一.概述 先上 ...

  4. java虚拟机学习-JVM内存管理:深入Java内存区域与OOM(3)

    概述 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又 ...

  5. 《深入理解Java虚拟机》-----第2章 Java内存区域与内存溢出异常

    2.1 概述 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任 ...

  6. JVM 内存区域 (运行时数据区域)

    JVM 内存区域 (运行时数据区域) 链接:https://www.jianshu.com/p/ec479baf4d06 运行时数据区域 Java 虚拟机在执行 Java 程序的过程中会把它所管理的内 ...

  7. 5.JVM的内存区域划分

    一.JVM介绍 1. 什么是JVM? JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟 ...

  8. 读书笔记,《深入理解java虚拟机》,第二章,java内存区域与内存溢出异常

    第二节,运行时数据区域.    在这个章节中,作者给出了一个java虚拟机运行时数据区的框图,图的左侧是方法区和堆,这两个数据区是所有的线程所共享的.然后是虚拟机栈.本地方法栈.还有程序计数器,这三个 ...

  9. 程序或-内存区域分配& ELF分析 ***

    一.在学习之前我们先看看ELF文件. ELF分为三种类型: 1. .o 可重定位文件(relocalble file) 2. 可执行文件 3. 共享库(shared library) 三种格式基本上从 ...

随机推荐

  1. Redis云端架构深入浅出

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯云数据库 TencentDB 发表于云+社区专栏 作者介绍:邹鹏,腾讯云数据库Redis产品负责人,多年数据库.网络安全研发经验. ...

  2. linux下创建网卡配置

    大家都知道linux系统一般作为服务器来用,而且很多情况的设置都是需要通过字符界面修改配置文件来设置.比如说配置网卡IP是修改/etc下面的 ifcfg-eth0,如果配置文件没有了怎么办呢?本经验以 ...

  3. C++要点总结

    1.内联成员函数 1)隐式声明:将成员函数直接定义在类的内部 2)显式声明:inline标示 2)在类中,使用inline定义内联函数时,必须将类的声明和内联成员函数的定义都放在同一个文件中,否则编译 ...

  4. Java复习第三天

    Day06 1.二维数组定义格式? (1)元素是一维数组的数组. (2)格式: A:数据类型[][] 数组名 = new 数据类型[m][n]; B:数据类型[][] 数组名 = new 数据类型[m ...

  5. java中list、set、map区别(转)

    Collection├List│├LinkedList│├ArrayList│└Vector│ └Stack└SetMap├Hashtable├HashMap└WeakHashMap Collecti ...

  6. thinkjs crud练习

    今天看了下thinkjs(v2.1.7),做了一个简单的入门demo,基于mysql数据库增删改查,详细源码如下: 页面整体展示: 会员新增: 删除: 查询: 主页面: <!DOCTYPE ht ...

  7. springboot项目部署到独立tomcat的爬坑集锦

    目录 集锦一:普通的springboot项目直接部署jar包 集锦二:springboot项目不能直接打war包部署 集锦三:因为tomcat版本问题导致的lombok插件报错:Invalid byt ...

  8. TCP keepalive长连接心跳保活

    比如:客户端与服务端进行握手时,经常无法握手成功,收不到回复: 需要建立保活机制. 1. 服务端Linux服务器新增系统内核参数配置. 在/etc/sysctl.conf文件中再添加如: #允许的持续 ...

  9. iview框架modal中嵌套modal

    modal的使用是平级的,后面的会覆盖前面,如下<modal>111</modal><modal>222</modal>内容为222的弹框会在内容为11 ...

  10. new Date(年-月)时间是8点

    new Date('2018-02')获取的小时是8时解决new Date('2018-2')获取的小时是0时