C++ 动态内存管理

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;
}
  • malloc:一个参数(所开辟空间的大小),返回值为void*,需与free配对使用,防止内存泄漏。
  • realloc:可用于第一次开辟空间以及调整动态内存,两个参数(所调整空间的起始位置,所调整空间的大小)。注意:realloc必须接收返回值,因为空间有可能开辟失败,返回为空指针。
  • calloc:两个参数(开辟空间的个数,每一份空间的大小)。

C++动态内存管理


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

int* p4 = new int;   //开辟1个整型的空间
int* p5 = new int(3);   //开辟1个整型的空间并初始化为3
int* 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运算符来动态管理内存

深入理解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释放:
void Test()
{
    Array* p1 = (Array*)malloc(sizeof (Array));
    delete(p1);
}

这里写图片描述

  • 程序并没有什么问题。malloc并不会调用构造函数,delete调用了析构函数。
void Test()
{
    Array* p2 = new Array;
    free p2;
}
  • 如果用free释放并不会调用析构函数
  • 重点来了,问题就出在这—>new调用了构造函数,在构造函数中已经给p2开了空,但是free并不会调用析构函数,所以p2开出的空间没有被释放掉,这就导致了一个很严重的问题:内存泄露。这种情况很危险。
void Test()
{
    Array* p4 = new Array[3];
    delete[] p4;
}

这里写图片描述

  • new[N]:开辟空间,调用N次构造函数分别初始化每个对象。delete[]调用N次析构函数清理对象,释放空间。
  • 问题又来了,这N次是如何来的?程序中我们并没有指定delete[N],这个问题我们在下部分内容中会详述。

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


void * operator new (size_t size);
void operator delete (void* p); 
void * operator new[](size_t size); 
void operator delete[] (void* p);
  • 标准库函数operator new/operator delete的命名很容易让人混淆,它并不是运算符重载。实际上我们不能重定义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去释放,就不会调用析构函数,没调用析构函数可能就会出现“内存泄露”。

总结一下:

  • operator new/operator delete operator new[]/operator delete[] 和 malloc/free⽤法⼀ 样。
  • 他们只负责分配空间/释放空间,不会调⽤对象构造函数/析构函数来初始化/清理对象。
  • 实际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[] 析构函数调用细节剖析:
    这里写图片描述

定位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);
}

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/80229603