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

1. 定义operator new的约定

1.1 约定列举

我们以operator new开始。实现一个一致的operator new需要有正确的返回值,在没有足够内存的时候调用new-handling函数(见Item 49),并且做好准备处理没有内存可分配的情况。你也想避免无端的隐藏“正常”版本的new,但这是一个类接口的问题而不是实现需求问题;它会在Item 52中进行处理。

Operator new的返回值部分很简单,因为operator new事实上会尝试多次分配内存,在内次分配失败之后都会调用new-handling函数。这里的假设是new-handling函数可能会做一些事情来释放一些内存。只有在指向new-handling函数的指针为null的情况下,operator new才会抛出异常。

好奇的是,C++即使在请求0个byte的时候也需要operator new返回一个合法的指针。(这个听上去很奇怪的要求简化了语言中的某些事情。)这就是基本情况,一个非成员operator new的伪代码会是像下面这个样子:

 void* operator new(std::size_t size) throw(std::bad_alloc)
{ // your operator new might using namespace std; // take additional params if (size == ) { // handle 0-byte requests size = ; // by treating them as
} // 1-byte requests
while (true) {
attempt to allocate size bytes; if (the allocation was successful)
return (a pointer to the memory);
// allocation was unsuccessful; find out what the
// current new-handling function is (see below)
new_handler globalHandler = set_new_handler();
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}

把请求0个byte当作请求1一个byte来进行处理的诡计看上去让人厌恶,但这是简单的实现并且合法,而且能够工作,无论如何,你对0个byte的请求会有多频繁呢?

对于伪代码中将new-handling函数指针设为null,然后迅速的将其复原的地方,你可能看上去比较怀疑。不幸的是,没有其他方法直接获得new-handling函数的指针,所以你必须调用set_new_handler来发现这个函数是什么。看上去粗糙但却是有效的,起码对于单线程来说是有效的。在多线程环境中,你可能需要某种类型的锁来安全的操作new-handling函数背后的(全局)数据结构。

Item 49中讨论过了,在operator new中包含一个无限循环,上面的代码中将其展示了出来;“while(true)”就 表示一个无限循环。跳出循环的唯一方法是成功的分配内存或者让new-handling函数做到Item 49中描述的事情中的其中一件:有更多的内存可供分配,安装一个不同的new-handler,卸载new-handler,抛出一个异常,这个异常要么继承自bad_alloc要么源于失败返回。现在你应该清楚为什么new-handler必须做到这些事情中的一件的了,如果做不到,operator new中的循环永远不会终止。

1.2 由继承导致的问题

许多人没有意识到operator new成员函数是要被派生类继承的。这可能会导致一些有趣的并发症。在上面的operator new的伪代码中,注意函数尝试分配size个bytes。这再合理不过了,因为这是传递到函数中的参数。然而,正如Item 50中解释的,实现一个自定义内存管理器的最一般的原因就是为特定类的对象进行内存分配优化,而不是为类或者它的任何派生类。也即是,我们为类X提供了一个operaor new,这个函数的行为是为大小正好为sizeof(X)的对象进行调整,即不大也不小。然而由于继承的存在,可能发生通过调用基类中的operator new来为派生类对象分配内存:

 class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derived: public Base // Derived doesn’t declare
{ ... }; // operator new Derived *p = new Derived; // calls Base::operator new!

如果基类中的operator new设计没有处理这种情况,处理它的最好的方法将对“错误”数量内存的请求丢弃掉,而是转而使用标准operator new来处理,就像下面这样:

 void* Base::operator new(std::size_t size) throw(std::bad_alloc)

 {

 if (size != sizeof(Base)) // if size is “wrong,”
return ::operator new(size); // have standard operator
// new handle the request
... // otherwise handle
// the request here
}

“等一下”我听见你大叫,“你忘记检查病态但是可能发生的情况,也就是size为0的情况了!”事实上,我没有忘记。测试仍然在那里,只不过是将测试并入size同sizeof(size)的测试之中了。C++用神秘的方式进行工作,其中之一的方式就是规定所有独立对象的大小不能为0(见Item 39)。根据定义,sizeof(Base)永远不会为0,所以如果size为0,内存请求将由::operator new来处理,它会以一种合理的方式来处理这个请求。

1.3 定义operator new[]的约定

如果你想在一个类中控制数组的内存分配,你需要实现operator new的数组形式,operator new[]。(这个函数通常被叫做“array new”,因为很难确定“operator new[]”该如何发音)。如果你决定实现operator new[],记住所有你正在做的是分配一大块原生内存——你不能对不存在于数组中的对象做任何事情。事实上,你甚至不能确定数组将会有多少对象。首先,你不会知道每个对象有多大。毕竟,很有可能通过继承来调用基类的operator new[]去为派生类对象数组分配内存,派生类对象通常比基类对象要大。因此,你不能假设在Base::operator new[]内部被放入数组的对象的大小为sizeof(Base),这就意味着你不能假设数组中对象的数量为(请求的字节数)/sizeof(Base)。第二,传递给operator new[]的参数size_t有可能比填入对象的内存更多,因为正如Item 16中解释的,动态分配的数组有可能包含额外的空间来存放数组元素的数量。

2. 定义operator delete的约定

当实现operator new的时候需要遵守的约定就这么多。对于operator delete,事情更加简单。所有你需要记住的是C++总是保证delete null指针是安全的,所以你需要遵守这个规定。下面是实现非成员 operator delete的伪代码:

 void operator delete(void *rawMemory) throw()
{
if (rawMemory == ) return; // do nothing if the null
// pointer is being deleted
deallocate the memory pointed to by rawMemory;
}

这个函数的成员函数版本也是简单的,但是你需要确保检查正在被delete的对象的size。假设你的属于类的operator new将对错误数量内存的请求转发给了::operator new,你同样得将对“错误大小”的delete请求转发给::operator delete:

 class Base { // same as before, but now
public: // operator delete is declared
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void *rawMemory, std::size_t size) throw();
...
};
void Base::operator delete(void *rawMemory, std::size_t size) throw()
{ if (rawMemory == ) return; // check for null pointer if (size != sizeof(Base)) { // if size is “wrong,” ::operator delete(rawMemory); // have standard operator return; // delete handle the request } deallocate the memory pointed to by rawMemory; return; }

有趣的是,如果要被delete的对象派生自于一个没有虚析构函数的基类,那么传递给operator delete的size_t值有可能是不正确的。这就有了足够的理由来把你的基类中的析构函数声明为虚函数,但是Item 7中描述了第二个可能更好的原因。现在你需要注意的是如果你在基类中忽略了虚析构函数,operator delete函数的工作就有可能不正确。

3. 总结

  • operator new应该包含一个无限循环来尝试分配内存,如果不能满足对内存的请求应该调用new-handler,应该处理对0个byte的请求。类的特定版本应该处理比预期更大的内存块的请求。
  • operator delete中传递的指针如果是null,应该什么都不做。类特定版本需要处理比预期要大的内存块。

读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定的更多相关文章

  1. 读书笔记 effective c++ Item 16 成对使用new和delete时要用相同的形式

    1. 一个错误释放内存的例子 下面的场景会有什么错? std::]; ... delete stringArray 一切看上去都是有序的.new匹配了一个delete.但有一些地方确实是错了.程序的行 ...

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

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

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

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

  4. 读书笔记 effective c++ Item 54 让你自己熟悉包括TR1在内的标准库

    1. C++0x的历史渊源 C++标准——也就是定义语言的文档和程序库——在1998被批准.在2003年,一个小的“修复bug”版本被发布.然而标准委员会仍然在继续他们的工作,一个“2.0版本”的C+ ...

  5. 读书笔记 effective c++ Item 1 将c++视为一个语言联邦

    Item 1 将c++视为一个语言联邦 如今的c++已经是一个多重泛型变成语言.支持过程化,面向对象,函数式,泛型和元编程的组合.这种强大使得c++无可匹敌,却也带来了一些问题.所有“合适的”规则看上 ...

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

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

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

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

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

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

  9. 读书笔记 effective c++ Item 13 用对象来管理资源

    1.不要手动释放从函数返回的堆资源 假设你正在处理一个模拟Investment的程序库,不同的Investmetn类型从Investment基类继承而来, class Investment { ... ...

随机推荐

  1. Java面试01|JVM相关

    1.JVM内存查看与分析,编写内存泄露实例 堆区.栈区.方法区.本机内存都有可能内存溢出.在这里编写堆区内存溢出实例.如下(来自<深入理解Java虚拟机>一书. // -Xms20m -X ...

  2. struts2(一) struts2入门

    首先推荐一本书,虽然我还没看过,但是我以后肯定会看的,<Struts+技术内幕>提取密码:kg6w .现在只是停留在会使用struts2的层次,自己也想继续深入研究,但是感觉自己的知识面还 ...

  3. 算法模板——Dinic网络最大流 2

    实现功能:同Dinic网络最大流 1 这个新的想法源于Dinic费用流算法... 在费用流算法里面,每次处理一条最短路,是通过spfa的过程中就记录下来,然后顺藤摸瓜处理一路 于是在这个里面我的最大流 ...

  4. 使用D3 Geo模块画澳大利亚地图

    数据 数据可视化主要旨在借助于图形化手段,清晰有效地传达与沟通信息.因此做数据可视化前需要想明白2件事: 你有什么数据? 你要传达什么信息? 本文中的示例中,将以不同的颜色显示澳大利亚不同地区的客户数 ...

  5. js中call、apply、bind那些事

    前言 回想起之前的一些面试,几乎每次都会问到一个js中关于call.apply.bind的问题,比如- 怎么利用call.apply来求一个数组中最大或者最小值 如何利用call.apply来做继承 ...

  6. 通过代码在eclips中添加Maven Dependencies依赖包的简单方法

    条件是已经正确解压了maven包并配置好了环境变量: 然后新建一个maven项目,(可在other中找到) 然后打开最下边的配置文件pom.xml: 打开后在文本下边选项选pom.xml选项: 在&l ...

  7. SQL AlawaysOn 之四:故障转移集群

    声明,故障转移集群,仅安装在SQL服务器中,域服务器不能和SQL服务器一起加入集群. 1.添加故障转移集群,下一步 2.安装 3.在域控制服务器上的管理工具里打开故不障转移集群管理器,选择创建集群 4 ...

  8. iOS开发之JSON解析

    JSON解析步骤: - (NSArray *)products { if (_products == nil) { //第一步:获取JSON文件的路径: NSString *path = [[NSBu ...

  9. 学点Groovy来理解build.gradle代码

    在写这篇博客时,搜索参考了很多资料,网上对于 Groovy 介绍的博客已经特别多了,所以也就没准备再详细的去介绍 Groovy,本来也就计划写一些自己认为较重要的点.后来发现了 Groovy 的官方文 ...

  10. Cassandra存储time series类型数据时的内部数据结构?

        因为我一直想用Cassandra来存储我们的数字电表中的数据,按照之前的文章(getting-started-time-series-data-modeling)的介绍,Cassandra真的 ...