4. C++ 内存管理
1. 栈内存:存放local变量、const局部变量、params等,编译器自动分配、并在函数执行结束时自动释放
> 栈内存分配运算内置于处理器的指令集中,效率很高,但是内存容量有限。其操作方式与stack类似
2. 堆内存:动态new的内存,需要自行释放
> 分配方式类似于链表,分配速度较慢、地址不连续、容易碎片化
3. 静态存储区:主要存放static数据、global数据和const常量
> 内存在程序编译的时候就已经分配好,并且在程序的整个运行期间都存在
4. 文字常量区:存放常量字符串,程序结束后系统释放
5. 程序代码区:存放函数体的二进制代码,通常只读
内存分配的方式主要是前三种
栈内存分布方式
高地址→低地址。实参从右到左入栈、再压入RET地址、而局部变量则顺序压入,如以下代码
int func(int param1 ,int param2,int param3){int var1 = param1;int var2 = param2;int var3 = param3;return var1;}
如左图所示,parm3先入栈,而var3最后入栈,ESP是栈顶指针堆内存分布方式
更接近链表
这个问题与虚拟地址空间的分配规则有关,内存的一端放置了静态代码和静态数据,另一端则放置动态数据和可增长的栈
合理的方案就是各放一端向中间生长,堆栈之间有很大的空闲地址,那么堆向上涨(方便地址计算),栈往下涨(栈的起始位置确定,动态调整栈空间大小也不需要移动栈内数据)
高到低分别为 环境变量/stack/heap/Data(静态存储可读写区,包括init了的和unInit的)/Text实践:写C++进行地址管理,及debug的时候,清楚了解内存分配规则会非常有用
new
先申请再构造
A* p = new A;编译器转化为A* p;void* mem = operator new(sizeof(A)); // 1.分配 其内部调用malloc(n)分配足够空间p = static_cast<A*>(mem); // 2.转型p->A::A(); // 3.构造
注意,operator new是标准库函数(特殊命名),而非对new进行运算符重载(new/delete是不允许重载的)
delete
先析构再回收
delete p;↓A::~A(p); // 析构及析构函数内的回收operator delete(p); // 内部调用free(p),回收该对象占用的空间
注意,对象没了 指针还在,所以最好再加上 p=nullptr;
new[]和delete[]
为什么new[]的时候要写明数组大小,而delete[]的时候却不用?
> 其实cpp在分配数组空间时,会多分配4bytes的大小来保存数组的capacity... 在delete[]时取出这个数,就可以知道要调用析构函数多少次
operator new[] // 在数组对象上面多分配了4个字节用来保存数组的大小,但是最终返回的是数组首地址的指针,而非所有分配空间的起始地址operator delete[] // 从最后一个元素开始逐个逆序析构。需要释放的空间不仅包括对象数组,还包括其额外的4个字节
new[]与delete混用
如果我new[]申请的内存,然后只调用delete,会发生什么?
只会调用一次首对象的析构函数。如果是无需析构的原子类型(如int),那么new[]时就不会额外申请4个字节,delete时释放掉传入参数(数组首地址)的整块内存,并不会有问题
而如果是自定义类型,就可能会有问题了,一个是不调用dtor可能造成memory leak,一个是delete传入参数不是p-4,也就少释放了4个字节!造成堆破坏
按照标准,混用 delete 和 delete[] 是未定义行为
智能指针
- shared_ptr:内置引用计数器,如shared_ptr
p=make_shared (10, '9'); make_shared函数使用传递的参数在堆上构造给定类型的对象,另外shared_ptr对象是一个类类型的对象,它有着自己的生存期 - unique_ptr:独享对象,只有我能引用。相比shared_ptr支持管理动态数组 unique_ptr
.release .reset 释放 - weak_ptr:不影响引用计数
- allocator:将内存分配和对象构造分离开来。allocator
a; a.construct(p, args);