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

【effective c++读书笔记】【第8章】定制new和delete(2)

2015-08-19 09:57 369 查看
条款50:了解new和delete的合理替换时机

有人会想要替换掉编译器提供的operator new或operator delete,以下是几个理由:

a、用来检测运用上的错误。编程过程中会出现各种错误。这些错误导致内存泄露(memory leaks)、不确定行为产生、overruns(写入点在分配区块尾端之后)、underruns(写入点在分配区块尾端之前)等的发生。定制的operator new和operator delete可以避免这样的问题。
b、为了强化效能。编译器带的operator new和operator delete必须处理开辟大块内存、小块内存、大小混合型内存,必须考虑内存碎片问题,可能会导致无法满足大区快内存需求,即使有足够但分散的小区块自由内存。定制的operator new和operator delete可以避免这样的问题,有时还可以提升性能。
c、为了收集使用上的统计数据。自定义的operator new和operator delete使我们得以轻松收集你的软件如何使用其动态内存,分配区块的大小,寿命分布,内存以FIFO次序或LIFO次序或随机次序来分配和归还等信息。
d、为了增加分配和归还的速度。使用定制分配器,有加快程序速度的价值。
e、为了降低缺省内存管理器带来的空间额外开销。泛用型分配器往往(虽然并非总是)不只比定制型慢,还使用更多空间,因为它们常常在每一个分配区块上招引某些额外开销。针对小型对象开放的分配器,例如Boost库的Pool,本质上消除了这样的额外开销。
f、为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。X86体系结构上的double访问最快——如果它们是8-byte对齐。但是编译器自带的operator new并不保证分配double是8-byte对齐。这种情况下将内存分配方案替换为8-byte齐位保证版本,可导致程序效率大大提升。
g、为了将相关对象成簇集中。如果特定的某个数据结构往往被一起使用,我们希望在处理这些数据时将“内存页错误”(page faults)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这样就可以将它们成簇集中到尽可能少的内存也上。
h、为了获得非传统的行为。有时候我们希望operator new和delete做编译器附带版没做没做的某些事。例如,在归还内存时将其数据覆盖为0,以此增加应用程序的数据安全。
请记住:

有许多理由需要写个自定义的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。

条款51:编写new和delete时需固守常规

1、实现一致性operator new必须返回正确的值,内存不足时必得调用new-handling函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new。

下面是个non-memberoperator new伪码:

void* operator new(std::size_t size) throw(std::bad_alloc){
	using namespace std;
	if (size == 0) { //处理0byte申请
		size = 1; //将它视为1byte申请
	}
	while (true) {
		尝试分配size byte
		if (分配成功)
			return (一个指针,指向分配得来的内存)
		// 分配失败:找出目前的new-handling函数
		new_handler globalHandler = set_new_handler(0);
		set_new_handler(globalHandler);

		if (globalHandler) (*globalHandler)();
		else throw std::bad_alloc();
	}
}

上述伪码中将new-handling函数指针设为null后又立刻恢复原样,因为没有任何办法直接取得new_handling函数指针,所以必须调用set_new_handler找它出来。
operator new有个无穷循环,退出此循环的唯一办法是:内存被成功分配或new-handling函数做了一件描述于条款49的事情:让更多内存可用、安装另一个new_handler、卸除new-handler、抛出bad_alloc异常,或是承认失败而直接return。
2、operator new成员函数会被derived classes继承。针对class X而设计的operator new,其行为很典型地只为大小为sizeof(X)的对象而设计。一旦被继承下去,有可能base class的operator new被调用用以分配derived class对象:
class Base {
public:
	static void* operator new(std::size_t size) throw(std::bad_alloc);
	...
};
class Derived : public Base {//假设Derived未声明operator new
	...
};
Derived* p = new Derived;//这里调用的是Base::operator new

如果base class专属的operatornew并非被设计用来对付上述情况,处理此情势最佳做法是将“内存申请量错误”的调用行为改采用标准operator new:
void* Base::operator new(std::size_t size) throw(std::bad_alloc){
	if (size != sizeof(Base)) //如果大小错误
	    return ::operator new(size);//令标准的operator new起而处理
	...                             //否则在这里处理
}


如果打算控制class专属“array内存分配行为”,那么你要实现operator new的array兄弟版:operator new[]。如果决定写个operator new[],唯一要做的一件事就是分配一块未加工内存,因为你无法对array之内迄今尚未存在的元素对象做任何事情。实际上,你甚至无法计算那个array将含多少个元素对象。你不知道每个对象多大,毕竟base class的operator new[]有可能经由继承被调用,将内存分配给“元素为derived
class对象”的array使用。
3、operator delete的情况更简单,需要记住的唯一事情就是C++保证“删除null指针永远安全”,所以你必须兑现这项保证。
下面是non-member operator delete的伪码:
void operator delete(void *rawMemory) throw() {
	if (rawMemory == 0) return;
	现在,归还rawMemory所指内存
}

这个函数的member版本也简单,只需多加一个动作检查删除数量。万一你的class专属operator new将大小有误的分配转交::operator new执行,你必须也将大小有误的删除行为转交::operator delete执行:
class Base {
public:
	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 == 0) return 0; //检查null指针
	if (size != sizeof(Base)) {  //如果大小错误,令标准版operator delete处理此一申请
		::operator delete(rawMemory);
		return;
	}
	现在,归还rawMemory所指的内存;
	return;
}

如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么c++传给operatordelete的size_t数值可能不正确,operatordelete可能无法正确运作。
请记住:

operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”。
operator delete应该在收到null指针时不做任何事情。class专属版本则还应该处理“比正确大小更大的(错误)申请”。

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

1、当写了下面这样一个new表达式:
Widget* pw = new Widget;

共有两个函数被调用:一个是用以分配内存的operator new,一个是Widget的default构造函数。
假设第一个调用成功,第二个却抛出异常。步骤一所分配内存必须取消并恢复旧观,否则会造成内存泄漏。这时,客户没能力归还内存,因为Widget构造函数抛出异常,pw尚未被赋值,客户手上也就没有指针指向该被归还的内存。这个时候,取消步骤一,并恢复旧观的责任就落到C++运行系统身上。
运行期系统会高兴的调用步骤一所调用operator new的相应的operator delete版本,前提是它必须知道哪一个operator delete被调用(可能有许多个)。
如果目前面对的是拥有正常签名式的new和delete,并不是问题,因为正常的operator new对应于正常的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作用域中的典型签名式。

2、如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这个便是所谓的placement new。众多placement new中特别有一个是“接受一个指针指向对象该被构造之处”,那样的operator new长相如下:
void* operator new(std::size_t, void* pMemory) throw();

这个版本的new已被纳入C++标准程序库,只要#include<new>就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象。
3、类似于new的placement版本,operator delete如果接受额外参数,便称为placement deletes。如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复时就没有任何operator delete会被调用。placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。对着一个指针施行delete时绝不会调用placement
delete。这意味对所有placement new我们必须同时提供一个正常的operator delete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常被抛出)。
例子:
class Widget{
public:
	...
	static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
	static void* operator delete(void* pMemory, std::size_t size) throw();
	static void* operator delete(void* pMemory, std::ostream& logStream) throw();
	...
};

如果以下语句引发Widget构造函数抛出异常:
Widget* pw = new (std::cerr) Widget;

对应的placement delete会被自动调用,让Widget有机会确保不泄漏任何内存。
4、由于成员函数的名称会掩盖其外围作用域中的相同名称,derived classes中的operator news会掩盖global版本和继承而得的operator new版本,必须小心避免让class专属的news掩盖客户期望的其他news(包括正常版本)。
5、缺省情况下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

请记住:

当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
当你声明placement new和placement delete,请确定不要无意识地遮掩它们的正常版本。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: