C++内存结构
图片来源阿秀的学习笔记
- 栈:函数内局部变量可以存储在栈区,函数执行结束自动释放。栈区内区分配运算内置于处理器指令集中
- 堆:new分配的内存块,由应用程序控制,地址向上增长,一般由程序员分配释放,如果程序员不释放,程序结束时可能由OS回收。与数据结构中的堆是两回事,分配方式类似于链表
- 自由存储区:和堆比较像,但是不等价。用free来释放
- (文件映射段):包括动态库、共享内存等,从低地址开始向上生长(跟硬件和内核版本有关)
- 全局数据区、静态存储区:全局变量和静态变量被分配到同一块内存中。C++中,该区定义的变量若是没有初始化,则会被自动初始化。
- 常量存储区:存放常量,不允许修改,程序结束之后由系统释放。
- 代码区:存放函数体的二进制代码
堆和栈
堆和栈的区别
- 申请方式:栈由系统分配,堆是自己申请和释放的(在windos中,最好的方式是VirtualAlloc分配内存,不是在堆不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便但是速度快也最灵活)
2.申请大小: 栈和栈顶是之前预设好的,栈向栈底扩展大小固定(可以通过ulimit-a查看,ulimit -s修改);堆向高地址扩展,是不连续的内存区域,大小可以灵活调整 - 申请效率:栈由系统分配,速度快,不会有碎片;堆由程序员分配,速度慢并且有碎片。
【笔记】C++的内存管理:堆和栈:
栈就像我们去饭馆吃饭,只管点菜吃饭付钱吃饱就走,不用理会切菜、洗菜等工作,所以很快捷但是自由度小
堆就像我们自己做饭,比较麻烦,但是自由度大
- 大小:栈空间默认4M,堆一般1G-4G.
- 内存管理机制:堆是系统有一个空闲内存地址链表,系统收到程序申请的时候遍历这个链表寻找第一个空间大于申请空间的堆结点;将该结点空间分配给程序,系统会将多余部分重新放入空闲链表;对于栈是只要栈的剩余空间大于所申请的空间,系统为程序提供内存,否则报异常提示栈溢出
- 空间大小堆大小受限于计算机系统中有效的虚拟内存;栈是一块连续的内存区域,大小是OS预定好的,栈的大小在编译时确定。
- 碎片问题堆会造成碎片;栈不会,类似于一个先进后出的栈
- 生长方向堆向上生长,栈向下生长
- 分配方式堆是C++函数库提供的,机制很复杂,所以堆的效率比栈低很多;栈是系统提供的数据结构,系统专门分配了寄存器存放栈地址,栈操作有专门指令。
- 入栈:PUSH
- 出栈:POP
- 栈顶指针:esp
- 栈底指针:ebp
- 栈和堆中存储的内容:在函数调用的时候,第一个进栈的是函数调用语句的下一条指令地址,然后是函数的各个参数,在大多数C编译器中,参数是从右往左入栈的,然后是函数中的局部变量(先定义的先入栈,后定义的后入栈)。静态变量不入栈,本次调用结束之后局部变量先出栈,然后是参数,最后是栈顶指针指向最开始存的地址也就是主函数中的下一条指令,程序从这个点开始继续运行;堆一般是在堆的头部用一个字节存放堆的大小,存什么内容由程序员安排。
堆和栈的效率
- 操作系统在底层对栈提供支持、分配专门的寄存器存放栈的地址、栈出栈入栈十分简单、有专门的指令执行,所以栈的效率高也比较快
- 堆的操作是由C++函数库提供的,分配堆内存的时候需要一定的算法寻找合适大小的内存,获取堆的内容需要两次访问第一次访问指针,第二次根据指针保存的地址访问内存,所以堆比较慢。
malloc堆分配算法
什么是堆分配算法?
为了不使分配空间的时候地址冲突,需要一个算法来管理堆空间,这个算法就是堆的分配算法
空闲链表
把堆上各个空闲块按照链表的方式连接起来,当用户需要一块空间的时候,遍历整个链表,直到找到合适的大小将他拆分,用户释放空间的时候进行合并。
空闲链表包括头结构和空闲块,头结构记录了上一个和下一个空闲块的地址
位图
将整个堆分为大量的块,每个块大小相同,每个块大小相同。用户请求内存的时候分配整个块的数据:第一个块称为已分配区域的头,其余称为已分配区域的主体,用一个整数数组记录块的使用情况。
可以用两个位来记录一个块,11表示头,10表示主体,00表示空闲
- 优点:速度快,稳定性好避免用户越界读写破坏数据且不需要额外信息
- 容易产生碎片,如果堆很大,块很小位图将会很大。
对象池
如果每一次分配的空间大小都一样,可以按照这个标准为一个单位,将整个堆空间划分为大量小块,每次请求找到一个小块就可以了。对象池的管理方法可以采用空闲链表或者位图。
内存池
内存池是真正使用内存之前,先申请分配一定大小数量的的内存块作为备用,有新的需求的时候,就从内存池中分配出一部分内存块来使用,内存不够的时候再继续申请新的内存。可以避免内存碎片,使内存分配效率得到提升
内存池实现机制
allocate包装malloc,deallocate包装free
- 一般使一次20*2的申请,先用一半,然后留一半。首先客户端调用malloc()配置一定数量的区块(固定大小的内存块,通常为8的倍数)
假设有32bytes的区块,其中20个区块留给程序使用,1个区块交出,另外19个处于维护状态;剩余20个留给内存池。
- 客户端之后有内存请求,想申请(2064bytes空间,这时内存池只有2032bytes),就先将(10*64bytes)个区块返回,1个区块交出,另外九个处于维护状态。这时内存池空空如也。
- 接下来如果客户端还有内存需求,就需要调用malloc()配置空间,此时新申请的区块数量会增加一个随着配置次数越来越大的附加量,同样一半提供程序使用,另一半留给内存池。申请内存的时候永远先看内存池有无剩余,有的话就用上,然后挂在0-15号某一条链表上,要不然要重新申请。
- 如果整个堆空间都不够了,就会在原先已经分配区块中寻找能满足当前需求的区块数量,能满足就返回,不能满足就报bad_malloc异常
alloc函数
GC2.9下的alloc函数的一个比较好的分配器实现规则如下
- 维护一条0-15号的一共16条链表,0号表示8bytes,1号表示16bytes……15号表示128bytes。
- 如果在申请内存的时候申请的大小不是8的倍数,就找到刚好能满足内存大小的8的倍数的链表
- GC4.9之后也还有alloc函数,不过已经变成了_pool_alloc
内存泄漏
程序未能释放不再使用的内存就是内存泄漏。会导致性能下降直到逐渐用完,导致另一个程序失败。一般常说的内存泄漏是堆内存的泄漏。
避免方法
- 计数法:申请空间+1,释放空间-1,如果最后不为0就是内存泄漏
- 所有析构函数一定申明为虚函数
- 对象数组释放一定要用delete[]
- new ,delete和malloc,free成对出现
排除方法
Windows
- Visual Leak Detecter
- Bounds Checker
- CRT库
Linux
- valgrind
解决办法
智能指针
检查、定位内存泄漏
- 检查方法:在main函数最后面一行,加上一句_CrtDumpMemoryLeaks()。调试程序,自然关闭程序让其退出,查看输出:
输出这样的格式{453}normal block at 0x02432CA8,868 bytes long
被{}包围的453就是我们需要的内存泄漏定位值,868 bytes long就是说这个地方有868比特内存没有释放。
- 定位代码位置
在main函数第一行加上_CrtSetBreakAlloc(453);意思就是在申请453这块内存的位置中断。然后调试程序,程序中断了,查看调用堆栈。加上头文件#include <crtdbg.h>
内存对齐
- 分配内存的顺序按照声明的顺序
- 每个变量相对与起始位置偏移量必须是该变量类型大小的整数倍,不是整数倍就空出内存,知道偏移量是整数倍为止
- 整个结构体大小必须是里面变量类型最大值的整数倍
添加#pragma pack(n)后规则就变成了下面这样
- 偏移量要是n和当前变量大小中较小值的整数倍
- 整日大小要是n和最大变量大小中较小值的整数倍
- n值必须要是1,2,4,8…,其他值就按照默认分配规则
深拷贝和浅拷贝的区别
浅拷贝
拷贝一个指针,并没有开辟一个新地址,拷贝的指针和原来的指针指向同一块地址。如果原来指针所指向的资源释放了,那么再释放浅拷贝的指针的资源会出现错误。
深拷贝
不仅拷贝值,还会开辟出来一块新的空间用来存放新的值。即使原先的对象被析构,释放内存也不会影响到深拷贝的值。
自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。
代码来源阿秀的学习笔记
#include <iostream>
#include <string.h>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student(){
name = new char(20);
cout << "Student" << endl;
};
~Student(){
cout << "~Student " << &name << endl;
delete name;
name = NULL;
};
Student(const Student &s){//拷贝构造函数
//浅拷贝,当对象的name和传入对象的name指向相同的地址
name = s.name;
//深拷贝
//name = new char(20);
//memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
};
};
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
//浅拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffed0c3ec0
//~Student 0x7fffed0c3ed0
//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 ***
//深拷贝执行结果:
//Student
//copy Student
//~Student 0x7fffebca9fb0
//~Student 0x7fffebca9fc0
浅拷贝在对象拷贝创建的时候存在风险,被拷贝的对象析构释放资源之后,拷贝的对象又释放了一个已经释放的资源,所以出现了问题。