- placement new和placement delete在C++中可能并不常用,但是你需要了解它们
- 关于placement new语法可以参阅:https://blog.csdn.net/qq_41453285/article/details/104690501
一、正常签名式的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,请确定不要无意识(非故意)地掩盖了它们的正常版本