Effective C++条款52:定制new和delete——写了placement new也要写placement delete

一、正常签名式的new和delete

  • 对于使用new来创建对象时,其可能在两个地方会抛出异常:
    • ①当在调用new()时,new()函数中可能会抛出异常
    • ②如果new()函数没有抛出异常,但是后面对象的构造函数可能会抛出异常
  • 当new()函数没有抛出异常,而对象的构造函数抛出异常时,此时系统已经为该对象申请了内存(new()申请的),因此如果构造函数抛出异常了,那么系统需要释放new()操作所示申请的内存。如果:
    • 如果程序使用的是正常签名式的new/delete,那么在new()未抛出异常,而对象的构造函数抛出异常时,delete()会自动撤销new()所做的一切,使其恢复原状(释放内存等)
    • 如果程序使用的是非正常签名式的new/delete,那么后果位置
  • 正常签名式的new和delete代表为系统默认类型的new和delete,它们通常是成对的。例如:
//正常签名式的new(全局式的)

void* operator new(std::size_t size)throw(std::bad_alloc);

//正常签名式的delete(全局式的)

void operator delete(void* rawMemory)throw();


//正常签名式的delete(成员函数版的)

void operator delete(void* rawMemory,std::size_t size)throw();

演示说明

  • 下面我们创建一个Widget对象:
//正常签名式的new(全局式的)

void* operator new(std::size_t size)throw(std::bad_alloc);

//正常签名式的delete(全局式的)

void operator delete(void* rawMemory)throw();


class Widget {};


Widget* pw = new Widget; //使用正常签名式的new创建对象
  • 上面的Widget使用正常签名式的new创建对象,如果:
    • 如果new()函数抛出异常,那么程序终止(此时程序没有任何内存,不会造成内存泄漏)
    • 如果new()函数未抛出异常,而Widget构造函数抛出异常了,那么new()已经申请了内存,因此我们需要释放这块内存,否则就造成内存泄漏了
  • 因为如果构造函数抛出异常,pw指针尚未被赋值,客户端无法取得该指针归还内存,因此此处的内存归还操作就交给了C++运行期系统身上。其规则是:
    • 如果使用正常签名式的new创建对象,那么系统自动调用正常签名式的delete来释放new所申请的内存
    • 因此,我们的程序如果在new未抛出异常而构造函数抛出异常时,会自动调用delete来释放内存

二、placement new

  • 什么是placement new:对于一般的new函数而言,其只有一个参数(size_t),但是如果我们为new()函数多添加一个参数,那么我们就称这个new为placement new
  • 例如下面是一个Widget,其有:
    • 一个placement new(非正常形式的),其参数2用来记录相关分配信息
    • 一个正常形式的new,就是我们“一”中所介绍的正常签名式的
class Widget {

public:

    //placement new(因为其带有一个ostream的参数)

    static void* operator new(std::size_t size,std::ostream& logStream)

    throw(std::bad_alloc);


    //正常签名式的delete

    static void operator delete(void *rawMemory, std::size_t size)

    throw();

};
  • 一个特殊的placement new:
    • 关于这个placement new,详情可以参阅:https://blog.csdn.net/qq_41453285/article/details/103547699
    • 在C++中,当我们提起placement new(定位new)时,一般就是指这个特殊的placement new,这个new也是属于placement new其中的一种
    • 其参数2是一个指针,用来在指定的内存上来进行内存分配。其代码如下:
//placement new

void* operator new(std::size_t size,void* pMemory)throw();

placement new可能导致内存泄漏

  • 例如我们上面定义的Widget如下:
class Widget {

public:

    //非正常签名式的new(因为其带有一个ostream的参数)

    static void* operator new(std::size_t size,std::ostream& logStream)

    throw(std::bad_alloc);


    //正常签名式的delete

    static void operator delete(void *rawMemory, std::size_t size)
    
    throw();

};
  • 当我们使用下面的代码定义Widget时,其可能会产生内存泄漏。理由如下:
    •  如果placement new未抛出异常,那么不会有任何事情
    • 如果placement new未抛出异常,而当Widget的构造函数抛出异常时,此时pw指针,pw指针尚未被赋值,客户端无法取得该指针归还内存(此时出现的情况于“一”中的演示案例一样)。但是此时我们使用的是placement new创建的对象,因此C++系统回去查找是否有placement delete来进行恢复原状,但是Widget没有定义,因此运行期系统不知道如何取消并恢复原先对placement new的调用
//调用placement new版本创建Widget对象

//如果placement new未抛出异常,而Wiget构造函数抛出异常,那么此段代码会内存泄漏

Widget* pw = new (std::cerr) Widget;

解决placement new的错误,为其定义一个placement delete

  • 上面由于我们之定义了placement new,当new未出错而构造函数出错时,运行期系统找不到一个对应的placement delete来取消并恢复原先对placement new的调用
  • 因此,对于自定义的placement new,我们也需要自定义一个placement delete。其代码如下:
class Widget {

public:

    //placement new

    static void* operator new(std::size_t size, std::ostream& logStream)

    throw(std::bad_alloc);


    //placement delete(与上面的new是配对的)

    static void operator delete(void* pMemory, std::ostream& logStream)throw();


    //其他代码同上

};
  • 此时当我们调用placement new创建对象时,new为出错而类的构造函数出错时,其对调用placement delete来取消并恢复原先对placement new的调用

对于placement delete的调用

  • 当我们定义了placement new之后通常也定义了一个placement delete。因此此时类中存在了两种delete(一种为正常签名式的,一种为placement delete)
  • 那么当我们使用delete销毁对象时,调用的是正常签名式的delete,而非placement delete。例如:
class Widget {

public:

    //placement new

    static void* operator new(std::size_t size, std::ostream& logStream)
    
throw(std::bad_alloc);


    //placement delete(与上面的new是配对的)

    static void operator delete(void* pMemory, std::ostream& logStream)throw();


    //正常签名式的delete

    static void operator delete(void* pMemory)throw();


    //其他代码同上

};



//调用placement new创建对象

Widget* pw = new (std::cerr) Widget;


//调用正常签名式的delete删除对象

delete pw;

三、placement new对标准new的隐藏

  • 默认情况下,C++在全局作用域内提供了下面形式的operator new:

演示案例

  • 当我们的类定义了placement new之后,而没有为该类定义正常签名式的new,那么对于该类来说,其只能使用placement new创建对象,不能使用正常签名式的new,因为全局的正常签名式的new被隐藏了
  • 例如:
class Base {

public:

    //只定义了placement new

    static void* operator new(std::size_t size, std::ostream& logStream)

    throw(std::bad_alloc);


    //全局的正常签名式的new将被隐藏

};


class Derived :public Base {

public:

    //其继承了Base的placement new


    //自己还定义了正常签名式的new

    static void* operator new(std::size_t size)throw(std::bad_alloc);

};


int main()

{

    Base* pb = new Base; //错误,全局的正常签名式的new被隐藏了

    Base* pb2 = new(std::cerr) Base; //正常


    Derived* pd=new (std::cerr) Derived; //正常,使用Base的placement new创建对象

    Derived* pd2 = new Derived; //正常,其自己定义了正常签名式的new


    return 0;
    
}

四、封装一个Base class

  • 根据前面文章的所有总结,现在我们封装一个Base class,使其包含所有正常形式的new和delete
  • 当然,C++标准更新之后,全局new和delete将会更改,因此这个类也需要进行更新
  • 代码如下:
class StandardNewDeleteForms{

public:

    //正常签名式的new和delete

    static void* operator new(std::size_t size)throw(std::bad_alloc){

        return ::operator new(size);

    }

    static void operator delete(void* pMemory)throw() {

        ::operator delete(pMemory);

    }


    //placement new和placement delete

    static void* operator new(std::size_t size,void* ptr)throw(std::bad_alloc){

        return ::operator new(size,ptr);

    }

    static void operator delete(void* pMemory, void* ptr)throw() {

        ::operator delete(pMemory, ptr);

    }


    //nothrow new/nothrow delete

    static void* operator new(std::size_t size, const std::nothrow_t& nt)throw(){
    
        return ::operator new(size,nt);

    }

    static void operator delete(void* pMemory, const std::nothrow_t& nt)throw() {

        ::operator delete(pMemory);

    }

};
  • 客户端自己的类可以继承于这个类,获取其所有new与delete。如果自己定义了new和delete,为了防止隐藏StandardNewDeleteForms中的new与delete,可以使用using声明。代码如下:
class Widget :public StandardNewDeleteForms {

public:

    //使基类中所有new和delete在派生类中可见

    using StandardNewDeleteForms::operator new;

    using StandardNewDeleteForms::operator delete;


    //再定义自己的new与delete

    static void* operator new(std::size_t size, std::ostream& logStream)throw(std::bad_alloc);

    static void operator delete(void* pMemory, std::ostream& logStream)throw();

};

五、class对于operator new的使用总结

  • 对于new来说:
    • ①当class定义了正常签名式的new时,全局的正常签名式的new和placement new都将被隐藏,其创建对象只能使用自己的new创建对象
    • ②同上,当class定义了placement new时,全局的正常签名式的new和placement new都将被隐藏,其创建对象只能使用自己的placement new创建对象
    • ③当你的class创建了placement new之后,为了防止创建对象时,其构造函数抛出异常而不能进行处理,建议同时定义定义一个placement delete,对placement new创建对象时其构造函数抛出异常而做善后还原处理

六、总结

  • 当你写一个placement operator new时,请确定也写出了对象的placement operator delete。如果没有这样,你的程序可能会发生隐微而时断时续的内存泄漏
  • 当你声明placement new和placement delete,请确定不要无意识(非故意)地掩盖了它们的正常版本

猜你喜欢

转载自blog.csdn.net/www_dong/article/details/113857100