您的位置:首页 > 编程语言 > C语言/C++

【Effective C++】读书笔记 条款52:写了placement new 也要写placement delete

2017-05-18 21:13 471 查看

定制new 和 delete

条款52:写了placement new 也要写placement delete

1. new 操作中的内存泄漏

有如下一个new操作

A *pb = new A;  //A是一个自定义class类型


我们知道,对于一个new操作,一共有如下步骤,

1. 调用operator new请求分配内存。

2. 调用A的构造函数进行构造。

3. 返回指针

假设第一步已经操作成功,但是在B的构造函数被调用的时候却发生了异常,所以为了防止内存泄漏,步骤一中的内存分配必须取消来恢复原状。但是此时pb并未被赋值相应的值,客户代码是没有能力来进行内存释放的。所以这个善后工作就交给了C++运行期系统。C++运行期系统就会调用与operator new相应的operator delete版本来释放内存。这样就保证了不会造成潜在的内存分配。

2. 定义与placement new 相对应的 placement delete 来防止内存泄漏

placement new 和 placement delete

先解释一个术语,placement new。

我们知道,对于一个operator new,其第一个参数一定是size_t,但是我们想定义一个新的operator new,它所接受的参数就可能除了size_t之外还有其他类型。这就是placement new的一个含义。其是指的是,接受任意额外实参的operator new。

除此之外,对于C++自身而言,它还提供了一个版本的operaotr new。

//C++98版本 placement
void* operator new (std::size_t size, void* ptr) throw()

//C++11版本 placement
void* operator new (std::size_t size, void* ptr) noexcept;


这个版本的operator new 接受一个 void *类型的额外参数,函数功能只是返回这个指针地址,来指示new操作中第二个步骤的构造函数在此指针所指地址上进行构造对象。这个版本的operator new也被称为 placement new。事实上,这个术语的名称也是来自于此。

所以人们谈到,placement new的时候,大多数是提到的指唯一额外实参是 *的operator new,少数时候是指的接受任意额外实参的operator new。

与此术语相对应的有placement delete。同样有两个含义,一个是C++提供的额外接受一个void *的operator delete。

//C++98 placement (3)
void operator delete (void* ptr, void* voidptr2) throw();

//C++11 placement (3)
void operator delete (void* ptr, void* voidptr2) noexcept;


这个版本的placement delete 对应于相应的placement new,第二个 void 的参数无实际意义,主要是为了和额外接受void 参数的形成对应。函数功能就是简单的返回相应的函数指针。

第二个含义就是接受任意额外实参的operator delete。

提供相应的placement delete

根据第一部分,我们能够知到,在进行new操作的时候,如果构造函数出现异常,运行期系统有责任取消operator new的内存分配恢复原状。

此时运行期系统就会调用相对应的operator delete来进行善后工作。

对于C++自身提供的operator new我们当然不用操心它的善后工作。

但是对于我们自身定义的operator new,如何寻找相对应的delete操作呢?

对于自定义的placement new,运行期系统将会寻找参数个数和类型都与operator new相同的某个 operator delete来进行善后工作

因此,对于一个带有额外参数的operator new没有带有相同额外参数的对应版本operator delete,那么new的内存分配动作需要取消来恢复原状的时候,就不会有任何的operator delete被调用。

就书上的例子来看。假设编写一个class专属的operator new,要接收一个ostream,用来记录相关分配信息,同时又写了一个正常形式operator delete:

class Widget{
public:
……
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); //非正常形式的new

static void* operator new(std::size_t size) throw(std::bad_alloc);
//正常形式的new

static void operator delete(void* pMemory) throw();   //正常的delete形式
……
};


并且采用如下形式new一个Widget。

Widget* pw=new (std:cerr) Widget;//调用operator new,并传递cerr作为ostream实参


注意,这个语句在Widget构造函数抛出异常的时候会泄漏内存,正如我们之前所探讨的那样,构造函数发生异常,运行期系统会寻找operator new相同的某个 operator delete 来回收内存,但是很可惜,它将不会找到对应的函数,所以就不会有任何版本的operator delete来调用。这就造成了内存泄漏。

相应的改善方法如下:

class Widget{
public:
……
//其他如上例中

static void operator delete(void* pMemory,std::ostream &logStream) throw();    //非正常的delete形式,对应于 new 版本,防止内存泄漏
……
};


对于如上的类定义,在构造函数异常的时候就有了正确的版本进行delete的调用。也就不会造成隐蔽的内存泄漏。

3. 要考虑是否隐藏了operator new和operator delete的正常版本

C++在缺省情况下在global作用域内提供了三种形式的operator new。

void* operator(std::size_t) throw(std::bad_alloc);//normal new
void* operator(std::size_t, void*) throw();//placement new
void* operator(std::size_t, const std::nothrow_t&) throw();//nothrow new


这是C++98版本的,同样有C++11版本的,但是具体功能都差不多,在这里不关注这些问题。

如果我们在class内定义了自己的operator new和operator delete操作,那么根据C++的作用域规则,将会隐藏C++默认提供的global版本。

如下:

calss Base
{
...
public:
static void *operator new(std::size_t size,std::ostream &logStream);  //自定义operator new

static void *operator delete(void *ptr,std::ostream &logStream);  //自定义operator delete
...
}


对于如上的Base,如果我们执行如下语句是OK的。

Base *pb = new(std::cerr) Base; //OK


但是想要使用C++缺省提供的new版本,就会出错

Base *pb = new Base;    //错误,对应的缺省版本已经被隐藏


我们必须要考虑这种情况,在定义operator new和operator delete的时候,是否真的要隐藏默认版本不再使用。否则就需要设计相应的函数来保留这些函数。

一个做法是我们可以在class内定义与缺省版本相同参数的版本,并在函数内调用默认版本。

如下:

class Base
{
...
public:
static void *operator new(std::size_t size)
{
::operator new(size); //转调缺省版本
}

//其他版本的operator new和operator delete
...
};


这样子客户就依旧会拥有C++提供的operator new/delete 版本。

除此之外,Effective C++书中还提到建立一个base class,内含所有正常形式的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(std::bad_alloc)
{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(std::bad_alloc)
{return ::operator new(size,nt);}

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


如果想以自定义方式扩充标准形式,可以使用继承机制和using声明

class Widget: public StandardNewDeleteForms{
public:
//让这些形式可见
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;

//添加自己定义的operator new/operator delete
static void* operator new(std::size_t size, std::ostream& logStream) throw(std:;bad_alloc);
static void operator detele(std::size_t size, std::ostream& logStream) throw();
};


4. 总结

当编写一个placement operator new时,也要编写对应版本的placement operator delete。否则就可能造成隐蔽而时断时续的内存泄露。

当声明了placement new和placement delete时,确定自己是否真的想要掩盖正常版本。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息