• 重新认识new


    前言

    感谢大佬:https://www.cnblogs.com/luxiaoxun/archive/2012/08/10/2631812.html

    www.cplusplus.com

     

    因为这段时间在重新再次学习STL,在学习到deque时,遇到了allocator类,学习过程中又遇到operator new,发现现在我认识的new,已经不是我之间认识的那个new了,故我要好好认清她。

    之前从C转向C++,因此免不了的就是malloc / free 和 new / delete对比。

    1. malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

    2. 对于内部数据类型(int, char, double...)的对象而言,光用malloc / free已经可以满足动态对象的要求。而在非内部数据类型(即类)对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数,这时候malloc / free已经无法满足,这时候new / delete应允而生。

     

    而在面试中也会经常被问到,但是现在来看我了解还是皮毛。。。

    如果读到这的话,写的可能比较冗余 请耐心看下去  谢谢!!!

    new operator(delete operator) & operator new (operator delete)

    首先我们得弄清楚 new operator(delete operator) &  operator new (operator delete)它们不一样,不一样。。。

    new operator (delete operator) 就是new (delete)操作符operator new (operator delete)函数

     new operator

    (1)调用operator new分配足够的空间,并调用相关对象的构造函数
    (2)不可以被重载

     

    operator new
    (1)只分配所要求的空间,不调用相关对象的构造函数。当无法满足所要求分配的空间时,则
            ->如果有new_handler,则调用new_handler,否则
            ->如果没要求不抛出异常(以nothrow参数表达),则执行bad_alloc异常,否则
            ->返回null pointer
    (2)可以被重载
    (3)重载时,返回类型必须声明为void*
    (4)重载时,第一个参数类型必须为表达要求分配空间的大小(字节),类型为size_t
    (5)重载时,可以带其它参数

     operator new 函数

    请先注意operator new的version (1)、(2),对于version(3)后一节会单独拎出

    函数原型:

    1. void* operator new(std::size_t size);
    //分配size字节存储空间,返回指向新分配的存储空间的指针,失败抛出一个bad_alloc异常

     

    2.void* operator new(std::size_t size, const std::nothrow_t& nothrow_value);
    //与第一个相同,只是在内存分配失败时,不会抛出异常,而是返回一个null指针

     

    对于nothrow的相关东西,请移步于我的另一篇博文

    http://www.cnblogs.com/SimonKly/p/7826660.html

     

    3.void* operator new(std::size_t size, void* ptr) throw(); //placement new
    //简单返回ptr,不会分配空间

     

    这个版本的operator new重载方式,不同于前两个版本,而是单独拿出来的,被叫做placement new。是最难理解(我比较笨,琢磨了好久)

     

    默认的分配和释放函数是标准库中特殊组件,有以下性质:
    * 全局性: operator new三个版本都在全局命名空间声明
    * 隐式声明: 无论是否包含头<new>,分配版本((1)和(2))在C++程序的每个翻译单元中隐式声明。
    * 可重载: 分配版本((1)和(2))也是可替换的:程序可以提供自己的定义来替换默认提供的定义来产生上述结果,或者可以为特定类型重载。

     

     下面有一个 http://www.cplusplus.com 例子,虽然很简单,但是对于理解我认为有很好的帮助

     1 #include <iostream>
     2 #include <new>
     3 
     4 using namespace std;
     5 
     6 class Simon
     7 {
     8 public:
     9     Simon()
    10     {
    11         cout << "constructed [ " << this << " ]" << endl;
    12     }
    13 
    14 private:
    15     int s;
    16 };
    17 
    18 int main(int argc, char** argv)
    19 {
    20     cout << "1: ";
    21     Simon* s1 = new Simon;
    22     // 分配内存调用 operator new(sizeof(Simon));
    23     //然后构建对象 调用构造函数
    24 
    25     cout << "2: ";
    26     Simon* s2 = new(std::nothrow) Simon;
    27     // 分配内存调用 operator new(sizeof(Simon), std::nothrow);
    28     // 然后构建对象 调用构造函数
    29 
    30     cout << "3: ";
    31     new(s2) Simon;
    32     // 不分配内存调用 operator new(sizeof(Simon), s2);
    33     // 在s2的地址空间构建对象(可以从输出结果看出),调用构造函数
    34 
    35     cout << "4: ";
    36     Simon* s3 = (Simon*)operator new(sizeof(Simon));
    37     // 只分配内存 
    38     // 不构建对象
    39 
    40     delete s1;
    41     delete s2;
    42     delete s3;
    43 
    44     cout << endl;
    45     system("pause");
    46     return 0;
    47 }

     运行结果:

    总结:可以看出version (1)、(2)只是分配存储空间,而不构建对象 ,new operator分配空间时,调用version(1)or (2)来进行自定义化内存分配

     

    Notice:
    operator new可以显式调用为常规函数,但在C++中,new是具有非常特定行为的运算符:具有new运算符的表达式首先调用具有其类型说明符大小的函数operator new() 作为第一个参数,如果这是成功的,它会自动初始化或构造对象(如果需要的话)。

     

    为什么有必要写自己的operator new和operator delete?why?(其实我也不知道,,,)
      答案通常是:为了效率。缺省的operator new和operator delete具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此。具体可参考《Effective C++》中的第二章内存管理。

     

     placement new

    placement new其实就是operator new重载第三个版本,也不知道谁为什么叫其placement new,害的我还以为是一个新东西,之后发现就是 http://www.cplusplus.com (见下图)上,曰其为 placement 版本。。。

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

     placement new的执行忽视size_t参数,只返回还第二个参数。允许用户把一个对象放到一个指定的内存缓冲器中,参数p它就指向一个内存缓冲器。

     

    placement new使用步骤

    placement new主要适用于:
    在对时间要求非常高的应用程序中,因为这些程序分配的时间是确定的;长时间运行而不被打断的程序,以及执行一个垃圾收集器(GC, garbage collector)

     

    在很多情况下,placement new的使用方法和其他普通的new有所不同。这里提供了它的使用步骤。

    第一步 缓存提前分配

    有三种方式:

    1.为了保证通过placement new使用的缓存区的memory alignment(内存队列)正确准备,使用普通的new来分配它:在堆上进行分配
    class Task ;
    char * buff = new [sizeof(Task)]; //分配内存
    (请注意auto或者static内存并非都正确地为每一个对象类型排列,所以,你将不能以placement new使用它们。)

    2.在栈上进行分配
    class Task ;
    char buf[N*sizeof(Task)]; //分配内存

    3.还有一种方式,就是直接通过地址来使用。(必须是有意义的地址)
    void* buf = reinterpret_cast<void*> (0xF00F);

    第二步:对象的分配

    在刚才已分配的缓存区调用placement new来构造一个对象。
    Task *ptask = new (buf) Task

    第三步:使用

    按照普通方式使用分配的对象:

    ptask->memberfunction();

    ptask-> member;

    //...

    第四步:对象的析构

    一旦你使用完这个对象,你必须调用它的析构函数来销毁它。按照下面的方式调用析构函数:
    ptask->~Task(); //调用外在的析构函数

     

    这里是不是就像GC机制呢?

    你可以反复利用缓存并给它分配一个新的对象(重复步骤2,3,4),节省了申请空间的时间开销。

    第五步:释放

    如果你不打算再次使用这个缓存,你可以象这样释放它:delete [] buf;

    跳过任何步骤就可能导致运行时间的崩溃,内存泄露,以及其它的意想不到的情况。如果你确实需要使用placement new,请认真遵循以上的步骤。

     

    如下有一个简单程序:

    #include <iostream>
    
    using namespace std;
    
    class animal
    {
    public:
    #if 1        //用于演示,无默认构造函数
        animal() : num(0)
        {
            cout << "animal constructor default" << endl;
        }
    #endif
        animal(int _num) : num(_num)
        {
            cout << "animal constructor param" << endl;
        }
    
        ~animal()
        {
            cout << "animal destructor" << endl;
        }
    
        void show()
        {
            cout << this->num << endl;
        }
    
        void * operator new(size_t size, void *p)
        {
            return p;
        }
    
    private:
        int num;
    };
    
    
    int main(int args, char ** argv)
    {
        // 一个动态animal数组
        void *p = operator new(5 * sizeof(animal)); // 申请缓冲器
        animal *a = static_cast<animal *>(p);        // 转换类型
        
        // 2.对象构建
        for (int i = 0; i < 4; i++)
        {
            new(a + i) animal(i);// 调用重载构造
        }
        new(a + 4) animal;    //    也可以调用默认构造
    
        // 3.使用
        for (int i = 0; i < 5; i++)
        {
            (a + i)->show();
        }
    
        // 4.销毁对象
        for (int i = 0; i < 5; i++)
        {
            (a + i)->~animal();
        }
    
        // 5.回收空间
        delete[]p;
    
        cin.get();
        return 0;
    }

    运行结果:

    placement new 存在的理由

    1.用placement new 解决buffer的问题

    问题描述:用new分配的数组缓冲时,由于调用了默认构造函数,因此执行效率上不佳。若没有默认构造函数则会发生编译时错误。如果你想在预分配的内存上创建对象,用缺省的new操作符是行不通的。要解决这个问题,你可以用placement new构造。它允许你构造一个新对象到预分配的内存上。

    2.增大时空效率的问题

    使用new操作符分配内存需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。placement new就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。

     

    new 、operator new 和 placement new 区别

    (1)new :不能被重载,其行为总是一致的。它先调用operator new分配内存,然后调用构造函数初始化那段内存。

      new 操作符的执行过程:
        1. 调用operator new分配内存 ;
        2. 调用构造函数生成类对象;
        3. 返回相应指针。

     

    (2)operator new:只分配内存空间,要实现不同的内存分配行为,应该重载operator new,而不是new。

    operator new就像operator + 一样,是可以重载的。如果类中没有重载operator new,那么调用的就是全局的::operator new来完成堆的分配。同理,operator new[]、operator delete、operator delete[]也是可以重载的。


    (3)placement new:只是operator new重载的一个版本。它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。因此不能删除它,但需要调用对象的析构函数。

    如果你想在已经分配的内存中创建一个对象,使用new时行不通的。也就是说placement new允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。原型中void* p实际上就是指向一个已经分配好的内存缓冲区的的首地址

  • 相关阅读:
    HDU 1698 Just a Hook(线段树成段更新)
    HDU 1247 Hat's Words (map+string)
    python三大框架之一flask中cookie和session的相关操作
    python三大框架之一flask应用
    python三大框架之一(flask介绍)
    pandas中遍历dataframe的每一个元素
    Python中pandas dataframe删除一行或一列:drop函数
    gevent多协程运用
    利用selenium并使用gevent爬取动态网页数据
    使用selenium 模拟人操作请求网页
  • 原文地址:https://www.cnblogs.com/SimonKly/p/7826651.html
Copyright © 2020-2023  润新知