C++的new和delete详解

目录
  • 1、new和delete的内部实现
  • 2、placement技术
  • 3、new和delete运算符重载
  • 4、对象的自动删除技术

1、new和delete的内部实现

C++中如果要在堆内存中创建和销毁对象需要借助关键字newdelete来完成。比如下面的代码

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;
}

newdelete既是C++中的关键字也是一种特殊的运算符。

   void* operator new(size_t size);
   void* operator new[](size_t size);
   void  operator delete(void *p);
   void  operator delete[](void *p);

newdelete不仅承载着内存分配的功能还承载着对象构造函数的调用功能,因此上面的对象创建代码其实在编译时会转化为如下的实现:

   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++中为我们隐藏了多少细节啊!既然newdelete操作默认是从堆中进行内存分配,而且newdelete又是一个普通的运算符函数,那么他内部是如何实现呢?其实也很简单。我们知道C语言中堆内存分配和销毁的函数是malloc/free因此C++中对系统默认的newdelete运算符函数就可以按如下的方法实现:

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时则只会进行内存分配而不会执行任何构造就比如下面的代码:

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

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

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

2、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++中的仍然是使用newdelete来实现这种技术。newdelete除了实现默认的操作符外还重载实现了如下的操作符函数:

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 。通过这几个运算符我们就可以优雅的实现上述的功能:

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;
}

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

3、new和delete运算符重载

发现一个很有意思的事情就是越高级的语言就越会将一些系统底层的东西进行封装并形成一个语言级别的关键字来使用。比如C++中的new和delete是用于构建和释放堆内存对象的关键字,又比如go语言中chan关键字是用于进行同步或者异步的队列数据传输通道。 C++语言内置默认实现了一套全局newdelete的运算符函数以及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运算符重载我们总有如何下规则:

  • newdelete运算符重载必须成对出现
  • new运算符的第一个参数必须是size_t类型的,也就是指定分配内存的size尺寸;delete运算符的第一个参数必须是要销毁释放的内存对象。其他参数可以任意定义。
  • 系统默认实现了new/deletenew[]/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时是有可能产生风险的。

4、对象的自动删除技术

一般来说系统对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运算符在类中重载的原因吧。

到此这篇关于C++的new和delete详解的文章就介绍到这了,更多相关C++的new和delete内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入浅析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]; delet

  • C++中malloc与free、new与delete的详解与应用

    C++面试经常会问到关于malloc/free和new/delete的区别,网上有不同版本的解释,这里总结下并加上个人理解和使用. 两者相同点 1.都可以申请动态堆内存. 两者不同点 1.new/delete是C++的操作符,malloc/free是C/C++的标准库函数. 2.new申请的可以理解为对象,new时会调用构造函数,返回指向该对象的指针,delete时调用析构函数:malloc申请的只是内存,不是对象. 3.new/delete是保留字,不需要头文件支持:malloc/free需要

  • 深入理解C++中的new和delete并实现对象池

    深入理解new和delete new和delete称作运算符 我们转反汇编看看 这2个运算符本质也是相应的运算符的重载的调用 malloc和new的区别? 1.malloc按字节开辟内存的:new开辟内存时需要指定类型 new int[10] 所以malloc开辟内存返回的都是void* 而new相当于运算符的重载函数 operator new ->返回值自动转成指定的类指针 int* 2.malloc只负责开辟空间,new不仅仅有malloc的功能,可以进行数据的初始化 new int(20)

  • C++ new、delete(new[]、delete[])操作符重载需要注意的问题

    new.delete(new[].delete[])操作符的重载需要注意: 1.重载的 new.delete(或者 new[].delete[])操作符必须是类的静态成员函数(为什么必须是静态成员函数,这很好理解,因为 new 操作符被调用的时候,对象还未构建)或者是全局函数,函数的原型如下: 复制代码 代码如下: void* operator new(size_t size) throw(std::bad_alloc); // 这里的 size 为分配的内存的总大小 void* operato

  • C++ 动态内存分配详解(new/new[]和delete/delete[])

    一.为什么需要动态内存分配? 在C++程序中,所有内存需求都是在程序执行之前通过定义所需的变量来确定的. 但是可能存在程序的内存需求只能在运行时确定的情况. 例如,当需要的内存取决于用户输入. 在这些情况下,程序需要动态分配内存,C ++语言将运算符new和delete合成在一起. (1)特点 1.C++中通过new关键字进行动态内存申请 2.C++中的动态内存分配是基于类型进行的 3.delete关键字用于内存释放 (2)语法 ①变量申请: Type* pointer = new Type;

  • c++中new和delete操作符用法

    "new"是C++的一个关键字,同时也是操作符.当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间.调用构造函数.返回正确的指针.当然,如果我们创建的是简单类型的变量,第二步就会被省略. new用法: 1. 开辟单变量地址空间 1)new int; 开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数

  • C++表达式new与delete知识详解

    在C++中,new表达式用于动态创建对象,即在堆(自由存储区)空间上为对象分配内存,而程序员也要小心的使用这些申请来的内存空间,当不再使用时应该调用delete表达式来释放该存储空间并且将指针置零. 本文学习了如何动态创建对象,动态创建的对象与一般对象的区别,动态创建的对象的初始化以及释放动态分配的内存等知识点. C++中分配的内存大致有三类:静态存储区,栈内存和堆内存 其中,静态存储区是在程序编译阶段就已经分配好的,用于全局变量,static变量等:堆栈是比较常用的对象存储方式. new和de

  • C++中new和delete的介绍

    介绍 1.malloc,free和new,delete区别. a.malloc,free是C/C++的标准库函数.new,delete是c++的操作符. b.malloc申请的是内存,严格意义不是"对象",new申请的可以理解为"对象",new 时会调用构造函数,返回指向该对象的指针. c.对于class类型,必须用new/delete来创建和销毁,自动调用构造和析构函数,malloc/free无法胜任. 2.使用new遵循原则: a.用new申请的内存,必须用de

  • 浅析c++中new和delete的用法

    new和delete运算符用于动态分配和撤销内存的运算符 new用法: 1.开辟单变量地址空间1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5 2. 开辟数组空间一维: int *a = new int[100];开辟一个大小为100的整型数组空间二维: int **a = new int[5][6]三维

  • C++动态内存分配(new/new[]和delete/delete[])详解

    C++动态内存分配(new/new[]和delete/delete[])详解 为了解决这个普通的编程问题,在运行时能创建和销毁对象是基本的要求.当然,C已提供了动态内存分配函数malloc( )和free( ),以及malloc( )的变种(realloc:改变分配内存的大小,calloc:指针指向内存前初始化),这些函数在运行时从堆中(也称自由内存)分配存储单元,但是运用这些库函数需要计算需要开辟内存的大小,容易出现错误. 那么通常我们在C语言中我们开辟内存的方式如下: (void*)mall

  • C++中new和delete的使用方法详解

    C++中new和delete的使用方法详解 new和delete运算符用于动态分配和撤销内存的运算符 new用法:           1.     开辟单变量地址空间 1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5           2.     开辟数组空间 一维: int *a = new in

随机推荐