本篇文章主要介绍在C语言的内存管理的基础上,C++的内存管理方式
1.C\C++ 内存分布
C和C++内存分布主要是, 栈,堆,静态区(数据段),常量区(代码段),还有一块内存映射段。各个区别如下:
- 栈(Stack)由操作系统自动管理,自动分配、自动释放,不需要手动申请释放。存放函数局部变量、函数形参、函数返回地址、临时变量。栈空间很小,一般几 MB,生长方向向下(向低地址增长)。函数调用结束,栈帧直接销毁,局部变量立刻失效。栈内存不会产生内存泄漏,但容易栈溢出,比如递归过深、定义超大局部数组。
- 堆(Heap)由程序员手动管理,C 用
malloc/free,C++ 用new/delete。需要手动申请内存,使用完必须手动释放,否则发生内存泄漏。堆空间很大,远大于栈,生长方向向上(向高地址增长)。用于存放程序运行时才确定大小的数据,动态开辟的对象、数组都放在堆上。堆内存释放后要避免野指针问题。 - 静态区(数据段)分为初始化数据段、未初始化数据段 (BSS 段)。存放全局变量、static 静态变量。程序启动时分配,程序结束才释放,生命周期是整个程序运行周期。 初始化数据段:存放已经初始化的全局变量、static 静态变量; BSS 段:存放未初始化的全局变量、静态变量,程序加载时系统自动置 0。
- 常量区 / 代码段(Text 段)代码段存放程序编译后的机器指令(二进制代码);常量区一般附属在代码段,存放字符串字面量、
const修饰的全局常量。该区域内存只读,不允许修改,尝试写操作会触发程序崩溃。生命周期贯穿整个程序,程序退出由系统回收。注意:局部 const 变量不在常量区,在栈上。 - 内存映射段(mmap 映射区)位于堆和栈之间。用来做文件映射、动态链接库.so 加载、匿名内存映射。操作系统把磁盘文件直接映射到虚拟内存,程序可以直接像访问内存一样读写文件,不需要频繁 read/write 系统调用。也用于大内存分配,当 malloc 申请超大块内存时,系统不走堆,直接使用 mmap 开辟。这部分内存由操作系统管理。
下面我们结合代码和图片来理解一下:
2.C语言中动态内存管理方式:malloc/calloc/realloc/free
- malloc:申请 size 字节堆内存,内存不初始化。
void* malloc(size_t size); - calloc:申请 num 个、每个 size 字节的堆内存,分配后自动置 0。
void* calloc(size_t num, size_t size); - realloc:重新修改已有堆内存的大小,可以扩容也可以缩容,不一定在原地址操作。
void* realloc(void* ptr, size_t new_size); free:就是把开辟的空间归还给操作系统。
3.C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1new/delete操作内置类型
void Test() { // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请10个int类型的空间 int* ptr6 = new int[3]; delete ptr4; delete ptr5; delete[] ptr6; }3.2new和delete操作自定义类型
使用new 为自定义类型开辟空间的方法和内置类型一样,delete 释放空间的使用方法也是一样的,但是和内置类型的区别是:new为自定义类型开辟空间后会调用构造函数,delete在释放空间前会调用析构函数。
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间 //还会调用构造函数和析构函数 A* p1 = (A*)malloc(sizeof(A)); A* p2 = new A(1); free(p1); delete p2; // 内置类型是几乎是一样的 int* p3 = (int*)malloc(sizeof(int)); int* p4 = new int; free(p3); delete p4; A* p5 = (A*)malloc(sizeof(A) * 10); A* p6 = new A[10]; free(p5); delete[] p6; return 0; }4.operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是
系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过
operator delete全局函数来释放空间。
其次就是,operator new实际也是通过malloc来申请空间,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施 就继续申请,否则就抛异常。operator delete最终是通过free来释放空间的。
这里主要是说明,new在开辟空间后不需要像malloc那样检查是否为空,只要捕获异常即可。
5.new和delete的实现原理
5.1内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申
请空间失败时会抛异常,malloc会返回NULL。
5.2自定义类型
new原理:
1.调用operator new函数申请空间
2.在申请的空间上执行构造函数,完成对象的构造
delete原理:
1.在空间上执行析构函数,完成对象中资源的清理工作
2.调用operator delete函数释放对象的空间
new[ N ]原理:
1.调用operatornew[]函数,在operatornew[]中实际调用operatornew函数完成N个对象空间的申请
2.在申请的空间上执行N次构造函数
delete[ N ] 原理:
1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2.调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
6.定位new表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
比如下面的程序:
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; // 定位new/replacement new int main() { // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行 A* p1 = (A*)malloc(sizeof(A)); new(p1)A; //注意:如果A类的构造函数有参数时,此处需要传参 p1->~A(); free(p1); A* p2 = (A*)operator new(sizeof(A)); new(p2)A(10); p2->~A(); operator delete(p2); return 0; }定位new表达式使用较多的场景是配合内存池使用,因为内存池分配出的内存没有初始化,所以如
果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
7.malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地
方是:
1.malloc和free是函数,new和delete是操作符
2.malloc申请的空间不会初始化,new可以初始化
3.malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[ ] 中指定对象个数即可
4.malloc的返回值为void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型
5.malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
6.申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放