C++的动态内存管理你真的了解吗
作者:寄一片海给你
前言
想必大家对c语言的动态内存分配并不陌生,忘了的小伙伴也可以看看我的这篇文章C语言动态内存分配
c语言的动态内存分配由于有些地方用起来比较麻烦同时检查错误的机制不适合c++,因此c++引入new/delete操作符进行内存管理,下面我们来深入探讨c++为什么要引入new/delete
用法上
对内置类型
new/delete同样是在堆区申请释放空间
new和delete申请释放单个元素的空间,new[]和delete[]申请释放一块连续的空间,new与delete匹配,new[]与delete[]匹配。new后面直接跟类型,不需要强制类型转换。简单来说new/delete用于单个对象,new[]/delete[]用于多个对象。
对内置类型没有什么区别,new操作符和malloc函数一样,不会对空间初始化,唯一的不同在后面的底层原理会介绍
#include <iostream> using namespace std; int main() { int* p1 = (int*)malloc(sizeof(int)); int* p2 = (int*)malloc(sizeof(int) * 10); //new后面跟动态申请的类型 int* p3 = new int;//动态申请一个int型空间 int* pp3 = new int(1);//动态申请一个int型空间并初始化为1 //动态申请10个int型空间并初始化 int* p4 = new int[10]{ 1,2,3 }; free(p1); free(p2); delete p3; delete pp3; delete[] p4; }
对自定义类型
对自定义类型那就有很大的区别了,new一个自定义对象,在申请空间后还会调用构造函数初始化,delete对象会先调用析构函数清理对象中的资源,然后释放空间
#include <iostream> using namespace std; class Test { public: Test(int data = 10) : _data(data) { cout << "Test():" << endl; } ~Test() { cout << "~Test():" << endl; } private: int _data; }; int main() { // 申请单个Test类型的空间 Test* p1 = (Test*)malloc(sizeof(Test)); free(p1); // 申请10个Test类型的空间 Test* p2 = (Test*)malloc(sizeof(Test) * 10); free(p2); // 申请单个Test类型的对象 Test* p3 = new Test; //申请空间并调用构造函数初始化 delete p3; // 申请10个Test类型的对象 Test* p4 = new Test[10];//申请空间并调用构造函数初始化 delete[] p4; return 0; }
简单解释一下:对于delete操作符先调用析构函数清理对象的资源,再释放空间,这里为了演示我只写了栈的构造函数和析构函数
#include <iostream> using namespace std; class Stack { public: Stack(int capacity = 4) :_capacity(capacity) { int* _p = new int[capacity]; _top = 0; _capacity = capacity; cout << "Stack()" << endl; } ~Stack() { delete[] _p; _top = _capacity = 0; cout << "~Stack()" << endl; } private: int* _p; int _top; int _capacity; }; int main() { Stack* s1=new Stack; delete s1; return 0; }
delete先调用构造函数清理对象维护的堆区B的资源,然后再释放堆区A的空间
new/delete底层原理
通过汇编代码我们发现,在底层:new会调用operator new函数和Stack构造函数,而delete也会调用析构函数和operator delete函数,那么operator new和operator delete是什么函数呢?
operator new和operator delete是系统提供的两个全局函数,通过operator new申请空间,operator delete释放空间,实际上operator new也是调用malloc申请空间的,operator delete也是调用free释放空间的,那么为什么不直接用malloc和free呢?
c语言中malloc申请空间失败会返回NULL空指针,那我们检查错误的方式就是通过errno错误码,而面向对象的语言,处理错误的方式一般是抛异常,C++中也要求抛异常——try catch,
这里我简单提一下抛异常,当我们new失败后,如果不捕获异常就会抛异常
int main() { //由于申请空间过大new失败 char* p = new char[1024u * 1024u * 1024u * 2 - 1]; printf("execute\n"); return 0; }
那我们捕获异常,new失败后编译器就会提示申请失败原因
#include <iostream> using namespace std; int main() { try //检测异常 { char* p = new char[1024u * 1024u * 1024u * 2 - 1]; //new失败,直接跳到catch,不执行下面语句 printf("execute\n"); } catch (const exception& e) //捕获并处理异常 { cout << e.what() << endl; } return 0; }
所以这就是为什么不直接使用malloc的原因,因为malloc申请空间失败不会抛异常
而operator delete也是在free函数的基础上增加了一些检查机制
operator new/operator delete用法与malloc/free函数类似,在定位new中会介绍
C++也有operator new[]和operator delete[],仅仅是为了和new[]和delete[]配对
重载类的专属operator new和 operator delete
operator new和operator delete是可以自己定义的,一般用于在STL中的内存池中申请空间,没学过内存池的小伙伴可以简单了解一下。
有个住在山上的少年,他每次洗澡,做饭,洗衣服需要用水的时候都需要跑到山下的河边接水,然后再回到山上,每天重复多次。那么可不可以在家附近建一个小水池,一次性将水池装满水,每次用水的时候就直接从水池打水就行了,不需要每次都跑到山下。这也就是内存池出现的原理,这里山下的小河可以看作操作系统的堆区,而每次打水可以看作是从堆区申请空间,水池可以看作内存池,这样我们直接从堆区申请一大块空间放在内存池,每次需要申请时,直接到内存池申请空间就行了,不需要每次都从堆区申请空间。
这里以双链表为例,如果没有内存池,每次创建新的节点时都需要从堆区申请空间,频繁的找操作系统申请会大大降低效率
我们直接在类里面定义operator new和operator delete,与系统提供的两个全局的函数构成重载,到时候new和delete就会调用我们定义的operator new和delete而不会调用那两个全局的函数
struct ListNode { ListNode* _next; ListNode* _prev; int _data; ListNode(int val = 0) :_next(nullptr) ,_prev(nullptr) ,_data(val) {} void* operator new(size_t n) { void* p = nullptr; p = allocator<ListNode>().allocate(1); //allocator是STL中的内存池 cout << "memory pool allocate" << endl; return p; } void operator delete(void* p) { allocator<ListNode>().deallocate((ListNode*)p, 1); cout << "memory pool deallocate" << endl; } }; class List { public: List() { _head = new ListNode; _head->_next = _head; _head->_prev = _head; } void ListPush(int val) { ListNode* newnode = new ListNode; newnode->_data = val; ListNode* tail = _head->_prev; tail->_next = newnode; newnode->_prev = tail; newnode->_next = _head; _head->_prev = newnode; } ~List() { ListNode* cur = _head->_next; while (cur != _head) { ListNode* next = cur->_next; delete cur; cur = next; } delete _head; _head = nullptr; } private: ListNode* _head; }; int main() { List l; l.ListPush(1); l.ListPush(2); l.ListPush(3); l.ListPush(4); return 0; }
定位new
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化
#include <iostream> using namespace std; class Test { public: Test(int data = 10) : _data(data) { cout << "Test():" << endl; } ~Test() { cout << "~Test():" << endl; } private: int _data; }; //构造函数是不支持直接手动调用的,所以引入定位new,对已经存在的对象调用构造函数初始化,常用于内存池中的对象 int main() { //调用operator new申请空间,operator new和malloc函数用法类似 Test* p = (Test*)operator new(sizeof(Test)); //调用构造函数初始化 new(p)Test(1); //定位new,new后面接要初始化对象的地址+类型,(1)表示初始化为1 //先调用析构函数,析构函数可以直接调用 p->~Test(); //在调用operator delete释放空间 operator delete (p); return 0; }
new/delete与malloc/free区别总结
共同点:都从堆上申请空间,并需要手动释放空间,因为new/delete可以说是对malloc/free的升级,申请/释放空间还是会调用malloc/free
不同点:
1、new/delete是操作符,而malloc/free是函数
2、首先是用法上的不同,malloc返回类型是void*,需要强转,而new不需要强转了,new后面直接跟类型,new也不需要计算空间大小,申请N个加[N],new/delete配对,mew[]/delete[]配对
3、malloc失败返回NULL指针,new失败如果不catch捕获就会抛异常
4、对内置类型,new和malloc一样也不会初始化,但对自定义类型,new会先申请空间再调用构造函数初始化,delete会先调用析构函数清理对象中的资源再释放空间
内存泄漏
动态申请的内存空间不使用了,又没有主动释放,就存在内存泄漏,当然不是所有的内存泄漏都有危害。
1.出现内存泄漏的进程正常结束,会将内存还给操作系统,不会有什么危害
2.但出现内存泄漏的进程非正常结束,比如僵尸进程,或是长期运行的程序,比如服务器程序,出现内存泄漏,那么危害就很大了,系统会越来越慢,甚至是卡死宕机
所以我们在new申请空间时,一定要记得delete释放空间
了解到这里我们就明白了c++为什么要引入new/delete了
1.对自定义类型,对象动态申请空间时,new/delete会自动调用构造函数/析构函数
2.new失败后抛异常,符合面向对象语言对出错的处理机制
3.我们可以定义类专属的operator new和operator delete,这样可以从内存池申请空间,避免频繁从堆区申请空间
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!