动态内存管理

来源:互联网 发布:js 浏览器窗口大小 编辑:程序博客网 时间:2024/06/06 13:40

1.C语言动态内存管理

    C语言为我们提供标准库函数malloc,realloc,calloc和free进行动态内存管理,我们可以先观察一下这三个函数的函数原型:

    void *malloc( size_t size);

    void *realloc( void *memblock,size_tsize);

    void *calloc( size_t num,size_tsize);

    以下简述这三个函数各自的特性:

int main(){    int* p1 =(int*)malloc(sizeof(int));    int* p2 = (int*)calloc(4, sizeof(int));    int* p3 = (int*)realloc(p2,sizeof(int)*6);    free(p1);    free(p2);    free(p3);    return 0;}

    1.malloc:一个参数(所开辟空间的大小),返回值为void*,需与free配对使用,防止内存泄漏。

    2.realloc:可用于第一次开辟空间以及调整动态内存,两个参数(所调整空间的起始位置,所调整空间的大小)。注意:realloc必须接收返回值,因为空间有可能开辟失败,返回为空指针。

    3.calloc:两个参数(开辟空间的个数,每一份空间的大小)。

2.C++动态内存管理

    我们都知道,C++是在C语言的基础上进行继承和发展的,所以在C++中我们同样可以使用标准库函数malloc,realloc,calloc和free进行动态内存管理。但C++还有更好的方法——new运算符。下面我们试一下这种新技术,若想要开辟一个整型的空间,new/delete运算符将为我们找到一个长度正确的内存块,并返回该内存块的地址。然后用指针来接收这个地址,下面是这样的一个示例:

int* p4 = new int;   //开辟1个整型的空间int* p5 = new int(3);   //开辟1个整型的空间并初始化为3int* p6 = new int[3];  //开辟3个整型的空间delete p4;   //new/delete匹配使用delete p5;delete[] p6;   //new[]/delete[]匹配使用

    运算符new/delete在使用上比malloc,realloc,calloc方便了很多,但是它们都做了同一件事:动态内存开辟。

    我们可以思考一件事:我们知道C++是兼容C的,那么已经有C库malloc/free等来动态管理内存,为什么 C++还要定义new/delete运算符来动态管理内存?

    下面我们就深入理解一下这件事:

3.深入理解C++动态内存管理

    malloc/free和new/delete的区别和联系?

int* p1 =(int*)malloc(sizeof(int));int* p4 = new int;

    上面我们提到new在使用上比malloc方便很多,体现在:malloc需要手动计算类型大小且返回值为void*,使用的时候我们需要将它强转成我们需要的类型;new可自己计算类型大小,并返回对应类型的指针。

    上面所述的内容都是在内置类型的基础上,C++是面向对象的语言,我们所使用的往往是自定义类型,下面我们自定义一个类来分析一下malloc和new其他的区别和联系:

class Array{public:Array(size_t size= 10)    : _size(size)    , _a(0){    cout << "Array(size_t size)" << endl;    if (_size> 0)    {        _a = new int[size];    }}~Array(){    cout << "~Array()"<< endl;    if (_a)    {        delete[]_a;        _a = 0;        _size = 0;    }}private:    int* _a;    size_t _size;};

    这样一个自定义类就创建好了。

    我们先看一下malloc:   

void Test(){    Array* p1 = (Array*)malloc(sizeof (Array));    free(p1);}


    内存开辟成功,再来看下new会有什么不同:

void Test(){    Array* p2 = new Array;    delete p2;}


    发生了什么?在运算符new动态内存开辟过程中竟然自动调用了构造函数和析构函数。

    这个过程是怎样发生的呢?

    通过程序调试,我们发现利用new来开辟空间,然后它会自动调用我们的构造函数进行初始化,然后调用析构函数做清理工作,最后delete释放空间。这又是malloc和new的一个大不同。这很好的体现了C++面向对象的语言的特点。

    接下来我们调换一下顺序:malloc开出的空间用delete释放、new开出的空间用free释放:

1.

 void Test(){    Array* p1 = (Array*)malloc(sizeof (Array));    delete(p1);}

    程序并没有什么问题。malloc并不会调用构造函数,delete调用了析构函数。

2.

void Test(){    Array* p2 = new Array;    free p2;}

    如果用free释放并不会调用析构函数。重点来了,问题就出在这--->new调用了构造函数,在构造函数中已经给p开了空间(p=开空间),但是free并不会调用析构函数,所以p开出的空间没有被释放掉,这就导致了一个很严重的问题:内存泄露。这种情况很危险。

void Test(){    Array* p4 = new Array[3];    delete[] p4;}

    new[N]:开辟空间,调用N次构造函数分别初始化每个对象。delete[]调用N次析构函数清理对象,释放空间。

    问题又来了,这N次是如何来的?程序中我们并没有指定delete[N].

    这个问题我们在下部分内容中会详述。

4.C++的其他内存管理接口

void * operatornew (size_t size);

void operatordelete (void* p);

void * operatornew [](size_t size);

void operatordelete[] (void* p);

    标准库函数operator new/operator deletede的命名很容易让人混淆,它并不是运算符重载。实际上我们不能重定义new和delete表达式的行为。

    我们可以试验一下:

Array* p5 = (Array*) new(sizeof(Array));


    再来看下面一段代码:(我们详细分析一下函数的调用步骤)

void Test1(){    Array* p1 = new Array;    delete p1;}

    详解如图:

    说明一点:对于内置类型而言,没有构造函数、没有析构函数,那么无论调new还是malloc都没有区别;why?调用malloc就相当于直接去开辟空间,new相当于是--->new会调用operator new、operator new里面又去调用malloc。也就是说对于内置类型而言,如果是new出来的空间,调用malloc去释放和调用delete去释放是一样的,最终都会调用free;

    但是对于自定义类型就不同了;如果用malloc开辟了空间,free去释放,就不会调用析构函数,没调用析构函数可能就会出现“内存泄露”。

总结一下:

    1.operator new/operator delete operator new[]/operator delete[] 和 malloc/free⽤法⼀ 样。

    2. 他们只负责分配空间/释放空间,不会调⽤对象构造函数/析构函数来初始化/清理对象。

    3. 实际operator new和operator delete只是malloc和free的⼀层封装。

    new: 1. 调⽤operator new分配空间;2. 调⽤构造函数初始化对象。

    delete: 1. 调⽤析构函数清理对象;2. 调⽤operator delete释放空间。

    new[N]: 1. 调⽤operator new分配空间。 2. 调⽤N次构造函数分别初始化每个对象。 

    delete[]:1. 调⽤N次析构函数清理对象。2. 调⽤operatordelete释放空间。

再回到我们上一小节所提出的问题上:程序中并没有指定delete[N]. 这N次是如何来的呢?

void Test3(){    Array* p4 = new Array[10];    delete[] p4;}

    按照我们自定义的类型,这里应该开辟80个字节的空间,为什么是84呢?结合前面的问题猜想一下,多出的这四个字节的空间有可能是用来存放对象个数的,如果真的是这样,那我们上一小节遗留的问题就可以解决了。我们可以验证一下:

    为什么要存对象的个数?原因就在于析构要去做一件事:它首先会去调用析构函数,其次会调用operator delete[ ](),调用operator delete[]()的时候根据p的地址在申请空间时多开了4个字节在头上,然后释放的时候也会减去4个字节,传给operator delete,operator delete再传给free。

    注意:内置类型(譬如int等)不需要调用构造函数和析构函数。而只有只有调用析构函数才会知道要调用多少次(由对象个数确定),才多开4个字节把对象的个数存下来;对于基本类型直接free就可以,压根不用多开4个字节。

    delete[] 析构函数调用细节剖析

5.定位new表达式

    定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。用法如下:

    new (place_address)

    type new (place_address)

    type(initializer-list) place_address必须是⼀个指针,initializer-list是类型的初始化列表。

void Test(){    //1.malloc/free + 定位操作符new()/显⽰调⽤析构函数,模拟 new和delete 的⾏为    Array* p1 = (Array*)malloc(sizeof(Array));    new(p1)Array(100);    p1->~Array();    free(p1);    //1.malloc/free + 多次调⽤定位操作符new()/显⽰调⽤析构函数,模拟 new[]和delete[] 的⾏为    Array* p2 = (Array*)malloc(sizeof(Array)* 10);    for(inti = 0; i < 10; ++i)    {        new(p2+ i) Array;    }    for(int i = 0; i < 10; ++i)    {        p2[i].~Array();    }    free(p2);}