《Effective C++》学习笔记(条款52:写了placement new 也要写 placement delete)

最近开始看《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 ,请确定不要无意识(非故意)地遮掩了它们的正常版本

条款53:不要轻忽编译器的警告

猜你喜欢

转载自blog.csdn.net/qq_34168988/article/details/121526839