最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
placement new 和 placement delete 在C++中并不常见,如果你不熟悉它们,也不用担心。当你写一个 new 表达式时:
Widget* pw = new Widget;
共有两个函数被调用:一个是以分配内存的 operator new,一个是 Widget
的默认构造函数。
假如第一个函数调用成功,但第二个函数却抛出异常,这时需要释放第一步分配得到的内存,否则就造成了内存泄漏。这个时候,用户没有能力去归还内存,因为如果 Widget
构造函数抛出异常,那么 pw 尚未被赋值,用户手中的指针还没有指向开辟的内存。因此释放第一步分配得到的内存的任务落到了C++运行期系统身上。
运行期系统会调用第一个函数 operator new 所对应的 operator delete 版本,前提当然是它必须知道哪一个 operator delete(因为可能有多个)版本。如果目前面对的是拥有正常签名式的 new 和 delete,这并不是问题,正常的 operator new 和对应于正常的 operator delete:
void *operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void* rawMemory) throw(); //global 作用域中正常的签名式
void operator delete(void* rawMemory,std::size_t size) throw(); //class 作用域中典型的签名式
如果使用正常的 operator new 和 operator delete,运行期系统可以找到如何释放 new 开辟内存的 delete 函数。但是如果使用非正常形式的 operator new,究竟使用哪个 delete 的问题就出现了。
举个例子,假设编写一个 class 专属的 operator new,要求接受一个 ostream,用来记录(logged)相关分配信息,同时又写了一个正常形式的 class 专属 operator delete:
class Widget{
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);//非正常形式的new
static void operator delete(void* pMemory, std::size_t size) throw(); //正常的class专属delete
...
};
这个设计有问题,但我们探讨原因之前,需要先绕道,简单讨一些术语。
如果 operator new 接受的参数除了必有的 size_t 之外还有其他,这便是 placement new。因此,上述的 operator new 是个 placement 版本。众多 placement new 版本中,有一个特别有用的是 “接受一个指针指向对象该被构造之处”,那样的 operator new 形式如下:
void* operator new(std::size_t, void* pMemory) throw(); //placement new
placement new 有多重定义,一是带任意额外参数的new ,二是只有一个额外参数 void*。当人们谈到 placement new ,大多数是指后者。
现在让我们回到 Widget
的声明式,也就是之前我说设计有问题的那个。这里的难点是,那个类将引起微妙的内存泄漏。看下面的例子,它在动态创建一个 Widget
时将相关的分配信息记录(logs)于 cerr:
Widget* pw = new (std:cerr) Widget;//调用operator new并传递cerr作为ostream实参,这个动作会在Widget构造函数抛出异常时泄漏内存
如果内存分配成功,但是 Widget
构造函数抛出异常,运行期系统要释放 operator new 开辟的内存。但运行期系统不知道真正被调用的 operator new 如何运作,因此它无法释放内存。所以上述做法行不通。取而代之的是,运行期系统寻找参数个数和类型都与 operator new 相同的 operator delete,如果找到,那就是它的调用对象。上述代码中调用的 operator new 对应的 operator delete为:
void operator delete(void*, std::ostream&) throw();
类似于 new 的 placement 版本,operator delete 如果接收额外参数,便称为 placement delete。上面 Widget
没有 placement 版本的operator delete,所以运行期系统不知道如何释放 operator new 开辟的内存,于是什么都不做。所以,如果 Widget
构造函数抛出异常,不会有任何的 operator delete 被调用。
为了解决上述问题,Widget
有必要声明一个 placement delete,对应那个有记录功能(logging)的 placement new:
class Widget{
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMemory) throw();
static void operator delete(void* pMemory, std::ostream& logStream) throw();
...
};
这样改变之后,如果以下语句导致Widget
构造函数抛出异常,就不会造成内存泄漏了:
Widget* pw = new (std:cerr) Widget; //这次内存不在泄漏
如果 Widget
构造函数抛出异常,就会调用对应版本的placement delete;如果没有异常,就会调用正常形式的 operator delete,如下:
delete pw;
placement delete 只有在 placement new 调用构造函数抛出异常时才会被调用。对着一个指针(例如上述的pw)施行 delete 绝不会导致调用 placement delete。
结论: 如果要对所有于 placement new 相关的内存泄漏宣战,我们必须同时提供一个正常的 operator delete (用于构造期间无任何异常被抛出)和一个 placement 版本(用于构造期间有异常被抛出), placement 版本的额外参数必须和 operator new 一样。
需要注意的是,因为成员函数的名称会掩盖其外围作用域中相同名称的函数(见条款33),所以要小心避免 class 专属的 new 掩盖用户希望调用的 new。例如,你有一个 Base class,其中声明唯一一个 placement,用户会发现他们无法使用正常形式的 new:
class Base{
public:
...
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);//这个new会掩盖正常的global new
...
};
Base* pb = new Base; //错误,因为正常形式的operator new被掩盖
Base* pb1 = new (std::cerr) Base; //正确,调用Base的placement new
同样道理,派生类 的 operator new 会掩盖继承而来的 operator new 和 global 版本的 new:
class Derived: public Base{ //继承自先前的Base
public:
...
static void* operator new(std::size_t size) throw(std::bad_alloc);//重新声明正常形式的new
...
};
Derived* pd = new (std::clog) Derived; //错误,因为Base的placement new被掩盖了
Derived* pd1 = new Derived; //正确,调用了 Derived 的 operator new
条款33更详细讨论了这种名称遮掩问题。对于撰写内存分配函数,你需要记住的是,默认情况下C++在 global 作用域内提供以下形式的operator new:
void* operator new(std::size_t) throw(std::bad_alloc); //normal new
void* operator new(std::size_t, void*) throw(); //placement new
void* operator new(std::size_t, const std::nothrow_t&) throw(); //nothrow new(见条款49)
如果你在 class 内声明任何形式的 operator new ,它都遮掩上述这些标准形式。除非你想要阻止 class 的用户使用这些形式,否则请确保它们在你所生成的任何自定义 operator new 之外还可用。对于每一个可用的 operator new,也要确保提供了对应形式的 operator delete。如果你希望这些函数有着平常的行为,只要令你的 class 专属版本调用 global 版本即可。
完成上面所说的一个简单的做法是,建立一个基类,内含所有正常形式的new和delete
class StadardNewDeleteForms{
public:
//normal
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
static void* operator new(std::size_t size, void* ptr) throw(){
return ::operator new(size, ptr);
}
static void operator delete(void* pMemory, void* ptr) throw(){
::operator delete(pMemory, ptr);
}
//nothrow
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&) throw(){
::operator delete(pMemory);
}
};
如果想以自定义方式扩充标准形式,可以使用继承机制和using声明式(见条款33)取得标准形式:
class Widget: public StandardNewDeleteForms{ //继承标准形式
public:
//让这些形式可见
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();
...
};
Note;
- 当你写一个 placement operator new,请确定也写出了对应的 placement operator delete。如果没有这样做,就可能造成隐蔽的内存泄漏
- 当你声明 placement new 和 placement delete ,请确定不要无意识(非故意)地遮掩了它们的正常版本
《Effective C++》学习笔记(条款52:写了placement new 也要写 placement delete)_c++ placement delete什么时候被调用-CSDN博客