11. 堆和栈
堆(Heap)与栈(Stack)有两层含义:
程序内存布局场景下,堆与栈表示两种内存管理方式
数据结构场景下,堆与栈表示两种常用的数据结构
栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。
堆由程序员分配释放,若程序员不释放,程序结束时由系统回收。
11.1. 区别
管理方式
栈由操作系统自动分配释放,无需我们手动控制;
堆的申请和释放工作由程序员控制,容易产生内存泄漏。
空间大小
每个进程拥有的栈的大小要远远小于堆的大小。 理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64-bit 的 Windows 默认 1MB,64-bit 的 Linux 默认 10MB。
分配方式
堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。
动态分配由 alloc
函数进行分配,但是栈的动态分配和堆是不同的,其动态分配是由操作系统进行释放,无需我们手工实现。
生长方式
堆的生长方向向上,内存地址由低到高。
栈的生长方向向下,内存地址由高到低。
分配效率
栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是由 C/C++ 提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多 。
存放内容
栈:存放函数返回地址、相关参数、局部变量和寄存器内容等。
堆:一般在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排。
11.2. 内存分区
在 C++ 中,内存主要分为堆、栈、全局/静态存储区和常量存储区。
栈 :就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆 :就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
全局/静态存储区 :全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的,在 C++ 里面没有这个区分了,他们共同占用同一块内存区。
常量存储区 :这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
11.3. 构造函数的空间分配
静态构造 :编译器为对象在栈空间中分配内存(直接移动栈顶指针,挪出适当的空间),然后在这片内存空间上调用构造函数构造一个栈对象。
A a;
动态构造 :在堆上申请内存;在堆内存上构造对象;指针指向该堆内存。
A* pa = new A();
限制在堆上构造对象
要求不能在栈空间构造类对象,直接将构造函数声明为 private 是不行的。因为 new 表达式实际上也调用了构造函数,会报错 error: 'A::A()' is private
。
在栈空间构造对象,是由编译器分配内存空间的。当对象被使用完之后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数(比如,类的析构函数是 private),则编译器无法释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性(其实不光是析构函数,只要是非静态的函数,编译器都会进行检查)。 如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存 。
另一方面,考虑到类的继承,应将构造函数和析构函数声明为 protected ,然后提供一个静态函数来完成对象的构造。
此外,还要方便释放对象所占用的内存空间。delete 表达式会调用析构函数,如果析构函数不是 public,在类外无法直接访问。
1class A
2{
3protected:
4 A(){}
5 ~A(){}
6public:
7 static A* create()
8 {
9 return new A();
10 }
11 void destroy()
12 {
13 delete this;
14 }
15};
1A* pa = A::create();
2pa->destroy();
限制在栈上构造对象
将 new 和 delete 运算符重载,并声明为 private。
1class A
2{
3public:
4 A(){}
5 ~A(){}
6private:
7 void* operator new(size_t){}
8 void operator delete(void*){}
9};
11.4. 参考资料
堆与栈的区别
C/C++——堆栈的讲解
C++ 自由存储区是否等价于堆?
如何让类对象只在栈(堆)上分配空间?