C++的new和delete详解

new和delete的内部实现

C++中若是要在堆内存中建立和销毁对象须要借助关键字new和delete来完成。好比下面的代码程序员

class CA {
    public:
       CA()m_a(0){}
       CA(int a):m_a(a){}

       virtual void foo(){ cout<<m_a<<endl;}
       int m_a;
};

void main() {
       CA *p1 = new CA;
       CA *p2 = new CA(10);
       CA *p3 = new CA[20];

       delete p1;
       delete p2;
       delete[] p3;
}

复制代码

new和delete既是C++中的关键字也是一种特殊的运算符。数组

void* operator new(size_t size);
   void* operator new[](size_t size);
   void  operator delete(void *p);
   void  operator delete[](void *p);
复制代码

new和delete不只承载着内存分配的功能还承载着对象构造函数的调用功能,所以上面的对象建立代码其实在编译时会转化为以下的实现:bash

CA *p1 = operator new(sizeof(CA));  //分配堆内存
      CA::CA(p1);   //调用构造函数

      CA *p2 = operator new(sizeof(CA));  //分配堆内存
      CA::CA(p2, 10);   //调用构造函数
     
      CA *p3 = operator new[](20 * sizeof(CA));
      CA *pt = p3;
      for (int i = 0; i < 20; i++)
     {
         CA::CA(pt);
         pt += 1;
     }

     CA::~CA(p1);
     operator delete(p1);
     
     CA::~CA(p2);
     operator delete(p2);

     CA *pt = p3;
     for (int i = 0; i < 20; i++)
     {
          CA::~CA(pt);
          pt += 1;
     }
     operator delete[](p3);

复制代码

看到上面的代码也许你会感到疑惑,怎么在编译时怎么会在源代码的基础上插入这么多的代码。这也是不少C程序员吐槽C++语言的缘由:C++编译器会偷偷插入不少未知的代码或者对源代码进行修改和处理,而这些插入和修改动做对于程序员来讲是彻底不可知的! 言归正传,咱们还能从上面的代码中看出new和delete操做实际上是分别进行了2步操做:1.内存的分配,2.构造函数的调用;3.析构函数的调用,4.内存的销毁。因此当对象是从堆内存分配时,构造函数执前内存就已经完成分配,一样当析构函数执行完成后内存才会被销毁。 这里面一个有意思的问题就是当咱们分配或者销毁的是数组对象时,系统又是如何知道应该调用多少次构造函数以及调用多少次析构函数的呢?答案就是在内存分配里面。当咱们调用operator new[]来分配数组对象时,编译器时系统内部会增长4或者8字节的分配空间用来保存所分配的数组对象的数量。当对数组对象调用构造和析构函数时就能够根据这个数量值来进行循环处理了。所以上面对数组对象的分配和销毁的真实代码实际上是按以下方式处理的:异步

//  CA *p3 = new CA[20]; 这句代码在编译时其实会转化为以下的代码片断
     unsigned long *p = operator new[](20 * sizeof(CA) + sizeof(unsigned long));  //64位系统多分配8字节
     *p = 20;   //这里保存分配的对象的数量。
     CA *p3 = (CA*)(p + 1);
     CA *pt = p3;
     for (int i = 0; i < *p; i++)
     {
         CA::CA(pt);
         pt += 1;
     }


    // delete[] p3;   这句代码在编译时其实会转化为以下的代码片断
     unsigned long *p =  ((unsigned long*)p3)  - 1;
     CA *pt = p3;
     for (int i = 0; i < *p; i++)
     {
          CA::~CA(pt);
          pt += 1;
      }
      operator delete[](p);
复制代码

可见C++中为咱们隐藏了多少细节啊!既然new和delete操做默认是从堆中进行内存分配,并且new和delete又是一个普通的运算符函数,那么他内部是如何实现呢?其实也很简单。咱们知道C语言中堆内存分配和销毁的函数是malloc/free。所以C++中对系统默认的new和delete运算符函数就能够按以下的方法实现:函数

void * operator new(size_t size) {
     return malloc(size);
} 

void * operator new[](size_t size)
{
     return malloc(size);
}

void operator delete(void *p) {
     free(p);
}

void operator delete[](void *p)
{
    free(p);
}
复制代码

这里须要注意的是你在代码里面使用new关键字和使用operator new操做符所产生的效果是不同的。若是你在代码里面使用的是new关键字那么系统内部除了会调用operator new操做符来分配内存还会调用构造函数,而若是你直接使用operator new时则只会进行内存分配而不会执行任何构造就好比下面的代码:oop

CA *p1 = new CA;   //这里会分配内存和执行构造函数

   CA *p2 = operator new(sizeof(CA));   //这里只是执行了普通的堆内存分配而不会调用构造函数
复制代码

上述的伪代码都是在运行时经过查看汇编语言而得出的结论,我是在XCODE编译器上查看运行的结果,有可能不一样的编译器会有一些实现的差别,可是无论如何要想真实的了解内部实现原理仍是要懂一些汇编的知识为最好。性能

placement技术

系统默认的new关键字除了分配堆内存外还进行构造函数的调用。而实际中咱们可能有一些已经预先分配好的内存区域,咱们想在这些已经分配好的内存中来构建一个对象。还有一种状况是不但愿进行频繁的堆内存分配和释放而只是对同一块内存进行重复的对象构建和销毁。就以下面的代码:测试

char buf1[100];
CA *p1 = (CA*)buf1;
CA::CA(p1);
p1->foo();
p1->m_a = 10;


char *buf2 = new char[sizeof(CA)];
CA *p2 = (CA*)buf2;
CA::CA(p2);
p2->foo();
p2->m_a = 20;


p1->~CA();
p2->~CA();

delete[] buf2;

复制代码

能够看出代码中buf1是栈内存而buf2是堆内存,这两块内存区域都是已经分配好了的内存,如今咱们想把这些内存来当作CA类的对象来使用,所以咱们须要对内存调用类的构造函数CA::CA()才能够,构造函数的内部实现会为内存区域填充虚表指针,这样对象才能够调用诸如foo虚函数。可是这样写代码不够优雅,那么有没有比较优雅的方法来实如今一块已经存在的内存上来构建对象呢? 答案就是 placement技术。 C++中的仍然是使用new和delete来实现这种技术。new和delete除了实现默认的操做符外还重载实现了以下的操做符函数:ui

void* operator new(size_t  size, void *p)
{
   return p;
}

void* operator new[](size_t size, void *p)
{
   return p;
}

void operator delete(void *p1, void *p2)
{
   // do nothing..
}

void operator delete[](void *p1, void *p2)
{
   // do nothing..
}

复制代码

咱们称这四个运算符为 placement new 和 placement delete 。经过这几个运算符咱们就能够优雅的实现上述的功能:spa

char buf1[100];
CA *p1 = new(buf1) CA(10);   //调用 operator new(size_t, void*)
p1->foo();


char *buf2 = new char[sizeof(CA)];
CA *p2 = new(buf2) CA(20);     //调用 operator new(size_t, void*)
p2->foo();


p1->~CA();
operator delete(p1, buf1);  //调用 operator delete(void*, void*)

p2->~CA();
operator delete(p2, buf2);  //调用 operator delete(void*, void*)

delete[] buf2;

复制代码

上面的例子里面发现经过placement new能够很优雅的在现有的内存中构建对象,而析构时不能直接调用delete p1, delete p2来销毁对象,必须人为的调用析构函数以及placement delete 函数。而且从上面的placement delete的实现来看里面并无任何代码,既然如此为何还要定义一个placement delete呢? 答案就是C++中的规定对new和delete的运算符重载必须是要成对实现的。并且前面曾经说过对delete的使用若是带了operator前缀时就只是一个普通的函数调用。所以为了完成析构以及和new操做符的匹配,就必需要人为的调用对象的析构函数以及placement delete函数。 除了上面举的例子外placement技术的使用还能够减小内存的频繁分配以及提高系统的性能。

void main()
{
      for (int i = 0; i < 10000; i++)
      {
           CA *p = new CA(i);
           p->foo();
           delete p;
      }
}

复制代码

例子里面循环10000次,每次循环都建立一个堆内存对象,而后调用虚函数foo后再进行销毁。最终的结果是程序运行时会进行10000次的频繁的堆内存分配和销毁。很明显这是有可能会影响系统性能的并且还有可能发生堆内存分配失败的状况。而若是咱们借助placement 技术就能够很简单的解决这些问题。

void main()
{
      char *buf = new[](sizeof(CA));
      for (int i = 0; i < 10000; i++)
      {
            CA *p = new(buf) CA(i);
            p->foo();
            p->~CA();
            operator delete(p, buf);
      }
      delete[] buf;
}
复制代码

上面的例子里面只进行了一次堆内存分配,在循环里面都是借助已经存在的内存来构建对象,不会再分配内存了。这样对内存的重复利用就使得程序的性能获得很是大的提高。

new和delete运算符重载

发现一个颇有意思的事情就是越高级的语言就越会将一些系统底层的东西进行封装并造成一个语言级别的关键字来使用。好比C++中的new和delete是用于构建和释放堆内存对象的关键字,又好比go语言中chan关键字是用于进行同步或者异步的队列数据传输通道。 C++语言内置默认实现了一套全局new和delete的运算符函数以及placement new/delete运算符函数。不论是类仍是内置类型均可以经过new/delete来进行堆内存对象的分配和释放的。对于一个类来讲,当咱们使用new来进行构建对象时,首先会检查这个类是否重载了new运算符,若是这个类重载了new运算符那么就会调用类提供的new运算符来进行内存分配,而若是没有提供new运算符时就使用系统提供的全局new运算符来进行内存分配。内置类型则老是使用系统提供的全局new运算符来进行内存的分配。对象的内存销毁流程也是和分配一致的。 new和delete运算符既支持全局的重载又支持类级别的函数重载。下面是这种运算符的定义的格式:

//全局运算符定义格式
void * operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);

//类内运算符定义格式
class CA
{
  void * operator new(size_t size [, param1, param2,....]);
  void operator delete(void *p [, param1, param2, ...]);
};

复制代码

对于new/delete运算符重载咱们总有如何下规则:

  • new和delete运算符重载必须成对出现
  • new运算符的第一个参数必须是size_t类型的,也就是指定分配内存的size尺寸;delete运算符的第一个参数必须是要销毁释放的内存对象。其余参数能够任意定义。
  • 系统默认实现了new/delete、new[]/delete[]、 placement new / delete 6个运算符函数。它们都有特定的意义。
  • 你能够重写默认实现的全局运算符,好比你想对内存的分配策略进行自定义管理或者你想监测堆内存的分配状况或者你想作堆内存的内存泄露监控等。可是你重写的全局运算符必定要知足默认的规则定义。
  • 若是你想对某个类的堆内存分配的对象作特殊处理,那么你能够重载这个类的new/delete运算符。当重载这两个运算符时虽然没有带static属性,可是无论如何对类的new/delete运算符的重载老是被认为是静态成员函数。
  • 当delete运算符的参数>=2个时,就须要本身负责对象析构函数的调用,而且以运算符函数的形式来调用delete运算符。

通常状况下你不须要对new/delete运算符进行重载,除非你的整个应用或者某个类有特殊的需求时才会如此。下面的例子你能够看到个人各类运算符的重载方法以及使用方法:

//CA.h

class CA {
public:
    //类成员函数
    void * operator new(size_t size);
    void * operator new[](size_t size);
    void * operator new(size_t size, void *p);
    void * operator new(size_t size, int a, int b);
    
    void operator delete(void *p);
    void operator delete[](void *p);
    void operator delete(void *p, void *p1);
    void operator delete(void *p, int a, int b);
};

class CB {
public:
    CB(){}
};


//全局运算符函数,请谨慎重写覆盖全局运算符函数。
void * operator new(size_t size);
void * operator new[](size_t size);
void * operator new(size_t size, void *p) noexcept;
void * operator new(size_t size, int a, int b);

void operator delete(void *p);
void operator delete[](void *p);
void operator delete(void *p, void *p1);
void operator delete(void *p, int a, int b);

.......................................................
//CA.cpp


void * CA::operator new(size_t size) {
    return malloc(size);
}

void * CA::operator new[](size_t size)
{
    return malloc(size);
}

void * CA::operator new(size_t size, void *p) {
    return p;
}

void* CA::operator new(size_t size, int a, int b) {
    return malloc(size);
}

void CA::operator delete(void *p) {
    free(p);
}

void CA::operator delete[](void *p)
{
    free(p);
}

void CA::operator delete(void *p, void *p1) {
    
}

void CA::operator delete(void *p, int a, int b) {
    free(p);
}


void * operator new(size_t size) {
    return  malloc(size);
}

void * operator new[](size_t size)
{
    return malloc(size);
}

void * operator new(size_t size, void *p) noexcept {
    return p;
}

void* operator new(size_t size, int a, int b) {
    return malloc(size);
}

void operator delete(void *p) {
    free(p);
}

void operator delete[](void *p)
{
    free(p);
}

void operator delete(void *p, void *p1) {
    
}

void operator delete(void *p, int a, int b) {
    free(p);
}

..................................
//main.cpp

int main(int argc, const char * argv[]) {
    
    char buf[100];

    CA *a1 = new CA();   //调用void * CA::operator new(size_t size)
    
    CA *a2 = new CA[10];  //调用void * CA::operator new[](size_t size)
    
    CA *a3 = new(buf)CA();  //调用void * CA::operator new(size_t size, void *p)
    
    CA *a4 = new(10, 20)CA();  //调用void* CA::operator new(size_t size, int a, int b)
    
    
    delete a1;  //调用void CA::operator delete(void *p)
    
    delete[] a2;  //调用void CA::operator delete[](void *p)
    
    //a3用的是placement new的方式分配,所以须要本身调用对象的析构函数。
    a3->~CA();
    CA::operator delete(a3, buf);  //调用void CA::operator delete(void *p, void *p1),记得要带上类命名空间。

    //a4的运算符参数大于等于2个因此须要本身调用对象的析构函数。
    a4->~CA();
    CA::operator delete(a4, 10, 20); //调用void CA::operator delete(void *p, int a, int b)
    
    //CB类没有重载运算符,所以使用的是全局重载的运算符。
    
    CB *b1 = new CB();  //调用void * operator new(size_t size)
 
    
    CB *b2 = new CB[10]; //调用void * operator new[](size_t size)
    
    //这里你能够看到同一块内存能够用来构建CA类的对象也能够用来构建CB类的对象
    CB *b3 = new(buf)CB();  //调用void * operator new(size_t size, void *p)
    
    CB *b4 = new(10, 20)CB(); //调用void* operator new(size_t size, int a, int b)
    

    delete b1;  //调用void operator delete(void *p)

    
    delete[] b2;   //调用void operator delete[](void *p)
    
    
    //b3用的是placement new的方式分配,所以须要本身调用对象的析构函数。
    b3->~CB();
    ::operator delete(b3, buf);  //调用void operator delete(void *p, void *p1)
    
    //b4的运算符参数大于等于2个因此须要本身调用对象的析构函数。
    b4->~CB();
    ::operator delete(b4, 10, 20);  //调用void operator delete(void *p, int a, int b)
   
   return 0;
} 
复制代码

我是在XCODE上测试上面的代码的,由于重写了全局的new/delete运算符,而且内部是经过malloc来实现堆内存分配的, malloc函数申明了不能返回NULL的返回结果检测: void *malloc(size_t __size) __result_use_check __alloc_size(1); 所以有可能你在测试时会发生崩溃的问题。若是出现这个问题你能够尝试着注释掉对全局new/delete重写的代码,再运行查看结果。 可见若是你尝试着覆盖重写全局的new/delete时是有可能产生风险的。

对象的自动删除技术

通常来讲系统对new/delete的默认实现就能知足咱们的需求,咱们不须要再去重载这两个运算符。那为何C++还提供对这两个运算符的重载支持呢?答案仍是在运算符自己具备的缺陷所致。咱们知道用new关键字来建立堆内存对象是分为了2步:1.是堆内存分配,2.是对象构造函数的调用。而这两步中的任何一步都有可能会产生异常。若是说是在第一步出现了问题致使内存分配失败则不会调用构造函数,这是没有问题的。若是说是在第二步构造函数执行过程当中出现了异常而致使没法正常构造完成,那么就应该要将第一步中所分配的堆内存进行销毁。C++中规定若是一个对象没法彻底构造那么这个对象将是一个无效对象,也不会调用析构函数。为了保证对象的完整性,当经过new分配的堆内存对象在构造函数执行过程当中出现异常时就会中止构造函数的执行而且自动调用对应的delete运算符来对已经分配的堆内存执行销毁处理,这就是所谓的对象的自动删除技术。正是由于有了对象的自动删除技术才能解决对象构造不完整时会形成内存泄露的问题。

当对象构造过程当中抛出异常时,C++的异常处理机制会在特定的地方插入代码来实现对对象的delete运算符的调用,若是想要具体了解状况请参考C++对异常处理实现的相关知识点。

全局delete运算符函数所支持的对象的自动删除技术虽然能解决对象自己的内存泄露问题,可是却不能解决对象构造函数内部的数据成员的内存分配泄露问题,咱们来看下面的代码:

class CA
{
  public:
    CA()
    {
          m_pa  = new int;
          throw 1;
    }

  ~CA()
   {
         delete m_pa;
         m_pa = NULL;
   }

 private:
      int *m_pa;
};

void main()
{
     try
     {
           CA *p = new CA();
           delete p;  //这句代码永远不会执行
     }
     catch(int)
    {
          cout << "oops!" << endl;
    }
}
复制代码

上面的代码中能够看到类CA中的对象在构造函数内部抛出了异常,虽然系统会对p对象执行自动删除技术来销毁分配好的内存,可是对于其内部的数据成员m_pa来讲,由于构造不完整就不会调用析构函数来销毁分配的堆内存,这样就致使了m_pa这块内存出现了泄露。怎么解决这类问题呢? 答案你是否想到了? 那就是重载CA类的new/delete运算符。咱们来看经过对CA重载运算符解决问题的代码:

class CA
{
public:
    CA(){
        m_pa = new int;
        throw 1;
    }
    //由于对象构造未完成因此析构函数永远不会被调用
    ~CA()
    {
        delete m_pa;
        m_pa = NULL;
    }
    
    void * operator new(size_t size)
    {
        return malloc(size);
    }
    //重载delete运算符,把已经分配的内存销毁掉。
    void operator delete(void *p)
    {
        CA *pb = (CA*)p;
        if (pb->m_pa != NULL)
            delete pb->m_pa;
        
        free(p);
    }
    
private:
    int *m_pa;
};
复制代码

由于C++对自动删除技术的支持,当CA对象在构造过程当中发生异常时,咱们就能够经过重载delete运算符来解决那些在构造函数中分配的数据成员内存但又不会调用析构函数来销毁的数据成员的内存问题。这我想就是为何C++中要支持对new/delete运算符在类中重载的缘由吧。

相关文章
相关标签/搜索