C++内存分区
一、内存分几个区原因
系统运行时不能所有数据都读取到CPU寄存器,所以需要有缓存,缓存不够用了所以有多级缓存。缓存的存储器毕竟还是相对昂贵的,所以还有内存(也叫外存)。但是数据在内存中的是怎样存放,这又是一个问题。
- 首先从字节层面看:信息在计算机肯定是0/1的bit比特位形式存储,习惯是8bit作为一个字节作为一个有意义的存储单位,但是有些数据的最小存储单位是多个字节,例如:int类型。多字节的数据在内存或者寄存器存储方式可以分为:大端、小端。
- 整体内存层面看:内存中有执行的二进制代码程序、有代码运行时需要不断改变的变量、有代码运行调用但是不需要改变的常量、还有需要的数据等等。内存中的数据可以看做多小电容,不停读写修改会让这些电容充放电,充放电频繁会产生磁场对附近的其他数据影响。为了抵消这些影响,系统把内存分为多个区,这样可以有效提升数据的安全性。企业级服务器使用的一般都是带ECC(Error Correcting Code)错误检查纠正技术的内存。
二、主流说法
程序在内存中主要分几个区域,每个区域存储那些数据(此处把程序、变量等都看做数据),主要有以下几种说法:
- 第一种说法是:5个区域(堆区、栈区、全局/静态存储区、常量存储区、自由存储区)。
- 第二种说法是:5个区域(堆区、栈区、全局/静态区、(字符)常量存储区、程序代码区)。
- 第三种说法是:5个区域(bss是英文Block Started by Symbol的简称、text段是程序代码段、data包含静态初始化的数据、stack保存函数的局部变量和参数、heap保存函数内部动态分配内存)
- 第四种说法是:4个区域(堆区、栈区、全局/静态存储区、常量区)。
2.1相同部分对比
堆区 | 栈区 | 全局/静态存储区 |
|
|
|
注意:堆区数据有先进先出特性,栈区数据有后进先出特性,但是并不是说只能依次取数据。具体怎么取堆/栈区中间数据需要看堆/栈是怎么实现的。如果栈是用常见的最大/最小树结构实现,那么就是取中间数据后再对其进行排序即可。
2.2自由存储区、堆区
主要讨论点是:自由存储区(free store),堆区有什么区别
- 很多博客划分自由存储区与堆的分界线就是new/delete与malloc/free。
- 然而,尽管C++标准没有要求,但很多编译器的new/delete都是以malloc/free为基础来实现的。那么借以malloc实现的new,所申请的内存是在堆上还是在自由存储区上?
- 从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。
自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。 - 基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:
堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。
2.3堆区和栈区区别
堆区和栈区主要区别集中在以下几点:
- 管理方式不同。
- 空间大小不同。
- 能否产生碎片不同。
- 生长方向不同。
- 分配方式不同。
- 分配效率不同。
具体区别参考下表:
堆区 | 栈区 |
|
|
- 堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片。
- 由于没有专门的系统支持,效率很低。
- 由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。
- 所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。
- 所以,使用的时候尽量用栈,而不是用堆。
- 虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
- 无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的 结果,就算是在程序运行过程中,没有发生上面的问题,还是要小心,说不定什么时候就崩掉。
2.4数据结构中的堆栈
以上的内存分区中的堆栈并不对应数据结构中的堆/栈。
2.4.1数据结构中的堆
堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。
2.4.2数据结构中的栈
栈(stack)限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈。
栈原来的意思,如此才能把握本质。栈,存储货物或供旅客住宿的地方,可引申为仓库、中转站,所以引入到计算机领域里,就是指数据暂时存储的地方,所以才有进栈、出栈的说法。
栈的特点后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。
三、栈使用
对内存分区有一些了解,知道为什么内存中数据要分开存,然后了解大概可以分几个区即可。具体分几个,new的对象存储在哪里,malloc申请的内存在哪里这些细节需要看具体的编译器和操作系统。如果不是做编译器的,可以对这些有了解即可。
3.1栈空间分配和栈溢出
栈空间分配:从高地址向低地址依次分配栈变量/对象空间(注意:栈变量/对象的数据的存储顺序遵从大端、小端规则,这个大端小端和栈空间分配顺序不一定一致);
栈溢出:
- 根据栈空间分配规则,可以设想如果有多个变量/对象时,变量/对象定义(不是声明)的顺序决定这个变量/对象在栈空间的位置。
- 先定义的在高地址空间,后分配变量在低地址空间。
- 后分配的栈变量/对象发生溢出时,是向高地址溢出,会覆盖定义顺序在前面的栈变量内容(Windows visual studio)。
测试代码:
1 #include <iostream> 2 #include <string> 3 4 void stackOverflow(char *data) { 5 6 char fillData[18]; 7 memset(fillData, 'A', sizeof(fillData)); 8 int s = sizeof fillData; 9 //内存拷贝到data,实际拷贝数据量比data内存空间大 10 //溢出后向高地址空间覆盖 11 memcpy(data, fillData, sizeof(fillData)); 12 } 13 14 int main() 15 { 16 char mainData[6] = "hello"; 17 char overflowData[6]; 18 memset(overflowData, 'B', sizeof(overflowData)); 19 std::cout << "before:\t"<< mainData << std::endl; 20 //overflowData溢出后向高地址覆盖 21 //会覆盖mainData空间内容 22 stackOverflow(overflowData); 23 std::cout << "after:\t" << mainData << std::endl; 24 getchar(); 25 }
- 执行结果:
四、相关参考
非常感谢以下的博客文章: