1. 调用普通版本的operator new抛出异常会发生什么?

Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉。当你像下面这样实现一个new表达式的时候,回忆一下Item 16Item 17

 Widget *pw = new Widget;

两个函数会被调用:一个是调用operator new来分配内存,第二个是Widget的默认构造函数。

假设第一个调用成功了,但是调用第二个函数抛出了异常。在这种情况下,对步骤一中执行的内存分配必须进行回滚。否则就会发生内存泄漏。客户端代码不能释放内存,因为如果Widget构造函数抛出了异常,pw永远不会赋值。客户端就没有办法得到指向需要释放内存的指针。对步骤一进行回滚的责任就落在了C++运行时系统身上。

运行时系统很高兴去调用与步骤1中调用的operator new版本相对应的operator delete,但是只有在它知道哪个operator delete(可能有许多)是合适的被调用函数的情况下才能做到。如果你正在处理的new和delete版本有着正常的签名,那么这不是一个问题,因为正常的operator new,

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

对应着正常的operator delete:

 void operator delete(void *rawMemory) throw();     // normal signature
// at global scope void operator delete(void *rawMemory, std::size_t size) throw(); // typical normal signature at class scope

2. 调用自定义operator new抛出异常会发生什么?

2.1 一个有问题的例子

如果你正在使用普通形式的new和delete,运行时系统能够找到new对应版本的delete来执行回滚操作。然而,如果你开始声明非普通版本的new——也就是生成一个带参数的版本,“哪个delete才是new对应的版本”这个问题就出现了。

例如,假设你实现了一个类特定版本的operator new,它需要指定一个ostream来为内存分配信息进行记录,你同样实现了一个普通的类特定版本的operator delete:

 class Widget {
public:
... static void* operator new(std::size_t size, // non-normal
std::ostream& logStream) // form of new
throw(std::bad_alloc);
static void operator delete(void *pMemory, // normal class
std::size_t size) throw(); // specific form
// of delete
...
};

2.2 对相关术语的说明

这个设计是有问题的,但是在我们讨论原因之前,我们需要对相关术语进行说明。

当一个operator new函数带了额外的参数(除了必须要带的size_t参数)的时候,我们知道这是new的placement版本。上面的operator new就是这样一个placement版本。一个尤为有用的placement new是带有一个指针参数,指定对象应该在哪里被构建。它会像下面这个样子:

 void* operator new(std::size_t, void *pMemory) throw(); // “placement
// new”

这个版本的new是C++标准库的一部分,只要你#inlucde <new>就能够访问它。它也用来在vector的未被使用的空间中创建对象。它还是最早的placement new。事实上,这也是这个函数的命名依据:在特定位置上的new。这就意味着“placement new”被重载了。大多情况下当人们谈到placement new的时候,它们讨论的是这个特定的函数,也即是带有一个void *额外参数的operator new。少数情况下,它们讨论的是带有额外参数的任意版本的operator new。程序的上下文往往会清除这种模棱两可,但是明白普通术语“placement new”意味着带额外参数的任意new版本是很重要的事,因为“placement delete”(我们一会会碰到)直接派生自它。

2.3 如何解决问题

现在让我们回到对Widget 类的声明上来,我在前面说过这个设计是有问题的。难点在于这个类会发生微妙的内存泄漏。考虑下面的客户代码,在动态创建一个Widget的时候它将内存分配信息记录到cerr中:

 Widget *pw = new (std::cerr) Widget; // call operator new, passing cerr as
// the ostream; this leaks memory
// if the Widget constructor throws

还是上次的问题,当内存分配成功了,但是Widget构造函数抛出了异常,运行时系统有责任将operator new执行的分配工作进行回滚。然而,运行时系统不能够真正明白被调用的operator new版本是如何工作的,所以它不能够自己进行回滚操作。相反,运行时系统会寻找一个operator delete,它和operator new带有相同数量和类型的额外参数,如果找到了,那么这个就是它要调用的版本。在上面的例子中,operator new带有一个额外的参数ostream&,所以对应的operator delete就是:

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

同new的placement 版本进行对比,带有额外参数的operator delete版本被叫做placement delete。在这种情况下,Widget没有声明operator delete的placement 版本,所以运行时系统不知道如何对placement new的操作进行回滚。因此它不会做任何事情。在这个例子中,如果Widget构造函数抛出异常之后没有operator delete会被调用!

规则很简单:如果一个带了额外的参数operator new 没有与之相匹配的带有相同额外参数的operator delete版本,如果new的内存分配操作需要被回滚那么没有operator delete会被调用。为了消除上面代码的内存泄漏,Widget需要声明一个与记录日志的placement new版本相对应的placement delete:

 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; // as before, but no leak this time

对应的placement delete会被自动被调用,这就让Widget确保没有内存被泄漏。

3. 调用delete会发生什么?

然而,考虑如果没有异常被抛出的时候会发生什么,我们会在客户端代码中进行delete:

 delete pw; // invokes the normal
// operator delete

正如注释所说明的,这会调用普通的operator delete,而不是placement 版本。Placement delete只有在构造函数中调用与之相匹配的placement new时抛出异常的时候才会被触发。对一个指针使用delete(就像上面的pw一样)永远不会调用delete的placement版本。

这就意味着为了对new的placement 版本造成的内存泄漏问题进行先发制人,你必须同时提供operator delete的普通版本(在构造期间没有异常抛出的时候调用),以及和placement new带有相同额外参数的placement版本(抛出异常时调用)。做到这一点,在内存泄漏的微妙问题上你就永远不需要在辗转反侧难以入睡了。

4. 注意名字隐藏问题

顺便说一下,因为成员函数名字会隐藏外围作用域中的相同的名字(见Item 33),你需要小心避免类特定的new版本把客户需要的其他版本隐藏掉(包括普通版本)。例如如果你有一个基类只声明了一个operator new的placement 版本,客户将会发现它们不能再使用new的普通版本了:

 class Base {
public:
...
static void* operator new(std::size_t size, // this new hides
std::ostream& logStream) // the normal
throw(std::bad_alloc); // global forms
...
}; Base *pb = new Base; // error! the normal form of
// operator new is hidden Base *pb = new (std::cerr) Base; // fine, calls Base’s
// placement new

类似的,派生类中的operator new会同时把operator new的全局版本和继承版本隐藏掉:

 class Derived: public Base {         // inherits from Base above

 public:
...
static void* operator new(std::size_t size) // redeclares the normal
throw(std::bad_alloc); // form of new
...
};
Derived *pd = new (std::clog) Derived; // error! Base’s placement
// new is hidden
Derived *pd = new Derived; // fine, calls Derived’s
// operator new

Item 33中非常详细的讨论了这种类型的名字隐藏,但是为了实现内存分配函数,你需要记住的是默认情况下,C++在全局范围内提供了如下版本的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,                             // nothrow new —
const std::nothrow_t&) throw(); // seeItem 49

如果你在类中声明了任何operator new,你就会隐藏这些标准版本。除非你的意图是防止客户使用这些版本,否则除了任何你所创建的自定义operator new版本之外,确保这些标准版本能够被客户所用。对每个你所提供的operator new,确保同时提供相对应的operator delete。如果你想让这些函数的行为同普通函数一样,让你的类特定版本调用全局版本就可以了。

实现这个目的的一种简单的方法是创建一个包含所有new 和delete版本的基类:

 class StandardNewDeleteForms {
public:
// normal 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/delete
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()
{ return ::operator delete(pMemory, ptr); }
// nothrow new/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&) throw()
{ ::operator delete(pMemory); }
};

客户如果想在自定义版本的基础上增加标准版本,只需要继承这个基类然后使用using声明就可以(Item 33)获得标准版本:

 class Widget: public StandardNewDeleteForms {       // inherit std forms

 public:                                                               

 using StandardNewDeleteForms::operator new; // make those

 using StandardNewDeleteForms::operator delete;    // forms visible

 static void* operator new(std::size_t size,        // add a custom

 std::ostream& logStream) // placement new
throw(std::bad_alloc);
static void operator delete(void *pMemory, // add the corres
std::ostream& logStream) // ponding place
throw(); // ment delete
...
};

5. 总结

  • 当你实现operator new的placement版本的时候,确保实现与之相对应的operator delete placement版本。如果你不进行实现,有的程序会发生微妙的,间歇性的内存泄漏。
  • 当你声明new和delete的placement版本的时候,确保不要无意间隐藏这些函数的普通版本。

读书笔记 effctive c++ Item 52 如果你实现了placement new,你也要实现placement delete的更多相关文章

  1. 读书笔记 effective c++ Item 52 如果你实现了placement new,你也要实现placement delete

    1. 调用普通版本的operator new抛出异常会发生什么? Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉.当你像下面 ...

  2. 读书笔记 effctive c++ Item 20 优先使用按const-引用传递(by-reference-to-const)而不是按值传递(by value)

    1. 按值传递参数会有效率问题 默认情况下,C++向函数传入或者从函数传出对象都是按值传递(pass by value)(从C继承过来的典型特性).除非你指定其他方式,函数参数会用实际参数值的拷贝进行 ...

  3. 读书笔记 effective c++ Item 6 如果你不想使用编译器自动生成的函数,你需要明确拒绝

    问题描述-阻止对象的拷贝 现实生活中的房产中介卖房子,一个服务于这个中介的软件系统很自然的会有一个表示要被销售的房屋的类: class HomeForSale { ... }; 每个房产中介会立刻指出 ...

  4. 读书笔记 effective c++ Item 50 了解何时替换new和delete 是有意义的

    1. 自定义new和delete的三个常见原因 我们先回顾一下基本原理.为什么人们一开始就想去替换编译器提供的operator new和operator delete版本?有三个最常见的原因: 为了检 ...

  5. 读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定

    Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些 ...

  6. 读书笔记 effective c++ Item 49 理解new-handler的行为

    1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...

  7. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

  8. 读书笔记 effective c++ Item 11 在operator=中处理自我赋值

    1.自我赋值是如何发生的 当一个对象委派给自己的时候,自我赋值就会发生: class Widget { ... }; Widget w; ... w = w; // assignment to sel ...

  9. 读书笔记 effective c++ Item 12 拷贝对象的所有部分

    1.默认构造函数介绍 在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:这两个函数分别叫做拷贝构造函数和拷贝赋值运算符.我们把这两个函数统一叫做拷贝函数.从Item5中,我 ...

随机推荐

  1. linux文件系统详解

    最近在做磁盘性能优化,需要结合文件系统原理去思考优化方向,因此借此机会进一步加深了对文件系统的认识.在看这篇文章之前,建议先看下前面一篇关于磁盘工作原理的解读.下面简单总结一些要点分享出来: 一.文件 ...

  2. ubuntu桌面不显示菜单

    为什么?我也不知道,只记得之前在搜狐看了行尸走肉,然后第二次开机就看不到菜单了. 参照百度结果然后去尝试了一下,记过ok了,我的ubuntu是13.10. 图说: 看到这里就应该大概怎么做了. 1   ...

  3. python 接口自动化测试--框架定型(六)

    脚本执行步骤: 1.还原测试数据库: 2.读取接口用例CSV文件到数据库: 3.执行数据库中标记执行的用例: 4.对比预期结果,将测试结果写入数据库结果表中. 数据管理: 事先备份测试数据库,并搭建自 ...

  4. js模块化加载器实现

    背景 自es6以前,JavaScript是天生模块化缺失的,即缺少类似后端语言的class, 作用域也只以函数作为区分.这与早期js的语言定位有关, 作为一个只需要在网页中嵌入几十上百行代码来实现一些 ...

  5. 【MySQL】分页查询实例讲解

    MySQL分页查询实例讲解 1. 前言 本文描述了团队在工作中遇到的一个MySQL分页查询问题,顺带讲解相关知识点,为后来者鉴.本文的重点不是"怎样"优化表结构和SQL语句,而是探 ...

  6. ADO.NET温习(一)

    ADO.NET概述 本节复习如何使用ADO.NET访问C#程序中的数据,主要介绍如何使用SqlConnection类和OleDbConnection类连接数据库,以及断开与数据库连接.深入讨论命令对象 ...

  7. 设置int、float型数据的输出格式

    设置整型输出的有效位数,比如在输出时间时时.分.秒都是两位有效数字,如果是5分钟应该输出00:05:00.需要加上头文件<iomanip> cout<<setw(2)<& ...

  8. laravel框架中所用到的依赖注入

    用Laravel开发前前后后有2个月左右了,之前一直写Java,就像找到Java和PHP之前的共同点,用Java的某些原理去理解PHP会发现还是有很多共通之处的.Java的依赖注入已经是一个很常见的概 ...

  9. JD . 简单的网站构成、引入图标、去除 图片间距/加粗/倾斜/下划线/蓝色外边框 禁止文本拖拽、文字居中、做logo、模拟鼠标 、不使用hover外部css样式实现hover鼠标悬停改变样式

    模拟京东案例准备: 截图(效果图PSD文件) 搭建项目环境     (结构样式行为分离)   HTML 核心文件     index.html CSS       控制样式 base.css(基础样式 ...

  10. shell脚本监控目录下文件被篡改时报警

    思路: 目录下文件被篡改的几种可能: 1.被修改 2.被删除 3.新增文件 md5命令详解 参数: -b 以二进制模式读入文件内容 -t 以文本模式读入文件内容 -c 根据已生成的md5值,对现存文件 ...