大家好,我是小杰。
我的主页:畅游码海
欢迎关注,共同进步!
我们在手捧那两本经典的《C++ primer plus》和《C++ primer》书籍的时候,书上清楚地写着我们在堆中申请和释放内存的时候需要使用new和delete,new [] 和 delete [] 并且必须要配对使用。但是我们只是知道要记住,但是大多都是只知其然,而不知其所以然,那么今天就来深入的聊聊,为什么要配对使用,我要是不配对是使用的话会怎么样。
new和delete到底做了什么
我们先来看一个经典的图:
从图中我们可以清楚的看到:
一个C++应用程序在堆中分配内存的过程一共有三种方式:
- 应用程序—>C++标准库—>new、delete相关—>malloc和free—>HeapAlloc、VirtualAlloc(操作系统底层API)
- 应用程序—>new、delete相关—>malloc和free—>HeapAlloc、VirtualAlloc(操作系统底层API)
- 应用程序—>malloc和free—>HeapAlloc、VirtualAlloc(操作系统底层API)
- 应用程序—>HeapAlloc、VirtualAlloc(操作系统底层API)
我们可以看到以下几点:
- 操作系统的API是我们开发应用程序绕不过去的,但是直接调用操作 系统API也不是我们大多数情况需要的,因为我们更多的时候需要写出多平台的代码,而不是针对某一个操作系统来写
- 尽管我们调用new和delete,但是其本质还是调用的C语言库中使用的malloc和free两个函数(这是我们一会儿要着重关注和研究的)
一个对象的“出生入死”
Object* obj = new Object; //出生
··· ···
delete obj //消灭
这个是我们new 一个对象然后调用delete来删除这个对象,那么其本质是什么嘞
我们new的操作在编译器眼中大致是什么样子:
Object* obj;
try{
void* mem = operator new( sizeof(Object) ); //分配内存
obj = static_cast<Object*>(mem); //指针转换
obj->Objcect::Object(); //原地构造
}
catch( std::bad_alloc ){
//失败就不执行构造
}
operator new 本质上是分配内存,调用的是malloc函数,具体我就不详细写出来啦,感兴趣的可以联系我进行交流。我们delete的操作在编译器眼中大致是什么样子:
pc->~Object(); //先析构
operator delete(obj); //释放内存
operator delete本质上是释放内存,调用的是free函数,具体我也就不详细写出来啦。
再来看看new []和delete []
Object* obj = new Object[3]; //三次构造
··· ···
delete[] obj; //三次析构
可以从上面看出来,根据知识迁移可知,当我们申请的是一个对象数组的时候,也同样会经历这个过程,有几个对象就会调用几次构造函数,有几个对象就会调用几次析构函数。我们申请的内存数组会在数组的头部存储一些相关的信息,具体的大小和编译器的实现有关。但是都会记录这个数组的个数。因此,我们构造次数==析构次数
思考归纳
那么传说中的,如果我们不进行delete [] 来释放内存的话,为什么会出现内存泄漏呢?
其实本质上来说不是这样的,就算我们使用 delete 和 new [] 搭配起来,也不会在这里发生内存泄漏,因为数据头中标记了个数,所以一定会把这块儿内存free 掉的。那内存泄漏是指的什么?
其实这样会导致这三个对象的析构函数只调用一次,也就是最上面的那个析构函数,其他的两个的析构函数不会被调用,所以现在明白了吧,内存泄漏指的是在这个对象中申请的内存,由于没有调用析构函数,导致那块儿内存不会被释放所以泄漏了内存。
举一反三,如果我们new的不是对象而是内置类型,或者说对象中并不含有 指向其他堆内存的指针,那么理论上来说就算不使用delete也不会造成内存泄漏,但是我们平时还是要遵守,这是一个好的习惯,搭配起来的话就不会出现各种各样奇怪的问题,这其实是对自己好。