Effective C++ 第八章(定制new和delete)

条款50:了解new和delete的合理替换时机

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

      在“全局性的”或“类专属的”基础上合理替换缺省的new和delete的时机有:

     1 检测运用错误,例如自定义的operator news可以超额分配内存,以额外空间(位于客户所得区块之前或之后)放置特定的byte pattern(签名)。operator deletes就可以检查上述前面是否原封不动,偌否就表示在分配区的某个生命时间点发生了overrun或underrun,这时候operator delete可以记录这个事实以及那个惹事生非的指针。如下:

//定制型operator new

static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;

void* operator new(std::size_t size) throw(std::bad_alloc)
{
	using namespace std;
	size_t realSize = size + 2 * sizeof(int);  //增加大小,使其可以塞入两个signatures

	void* pMem = malloc(realSize);
	if (!pMem)
		throw bad_alloc();

	//将signature写入内存的最前段落与最后段落
	*(static_cast<int*>(pMem)) = signature;
	*(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;

	//返回指针,指向位于第一个signature之后的内存位置
	return static_cast<Byte*>(pMem) + sizeof(int);
}
      这个operator new有缺点,首先条款51(一会写呀)说所有的operator or news都应该内含一个循环,反复调用某个new-handling函数。这里却没有。还有一个是对齐问题,C++要求所有operator news返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作。如果operator new返回malloc返回的指针,则是安全的,但是上面这段代码对返回的指针进行了偏移,所以不一定能对齐,不一定安全。

     2 为了收集动态分配内存之使用信息(软件是如何使用其动态内存的等相关信息都可以在operator new和delete中收集到)

     3 为了增加分配和归还的速度。泛用型分配器往往比定制型分配器慢,特别是相对于为某个特定类型对象定制的分配器。类专属分配器是“区块尺寸固定”之分配器的实例。例如Boost提供的pool程序库。

     4 为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往还使用更多内存,因为它们场次在每一个分配区块身上招引某些额外开销,针对小型对象而开发的分配器(Boost的Pool)本质上消除了这样的额外开销。

     5 为了弥补缺省分配器中的非最佳奇位。某些编译器自带的operator news并不保证对动态分配而得的doubles采取8-byte对齐。将缺省的operator new替换为一个8-byte齐位保证版,可导致程序效率大幅提升

    6 为了将相关对象成簇集中。new和delete的“placement版本”有可能完成这样的集簇行为

    7 为了获得非传统的行为。例如可能会希望分配和归还共享内存的区块,但唯一能够管理该内存的只有C API函数,所以写一个定制版new和delete,使得以C API穿上C++外套。


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

        operator new需要返回正确的值,内存不足时必须调用new-handling函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new. 

        operator new的返回值:如果它有能力供应客户申请的内存,就返回一个指针指向的那块内存,如果没有能力,则抛出一个bad_alloc异常。operator new也不只一次尝试分配内存,并在每次失败后调用new-handling函数,这里是假设new-handling函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling函数的指针是null, operator new才会抛出异常。

        C++规定,即使客户要求0 byte, operator new也得返回一个合法指针。例如下面是一个non-member operator new伪码:

//编写new和delete时需固守常规

void* operator new(std::size_t size) throw(std::bad_alloc)
{
	using namespace std;
	if (size == 0)  //如果是0 byte, 则当成1byte
		size = 1;

	//循环尝试分配size bytes;
	while (true)
	{
        尝试分配size bytes;
		if (分配成功)
			return (一个指针,指向分配得来的内存);

		//分配失败,找出目前的new-handling函数,因为没有直接获得该值的函数,
		//所以使现在的handler=null,利用其返回值返回以前的handler;
		new_handler globalHandler = set_new_handler(0); 
		set_new_handler(globalHandler);  //安装

		//如果非空,则调用,失败则抛出异常
		if (globalHandler)
			(*globalHandler)();
		else
			throw std::bad_alloc();
	}
}

         如果写operator new是类成员函数版本,则要考虑如果类被继承,会出现问题,因为继承类可能会继承该函数,这时如果继承类调用new则调用的是继承而来的,但是这个定制型内存管理其是针对某个特定类的对象分配行为进行最优化,不是为了该类的继承类。即针对类X而设计的operator new,其行为很典型的只是为大小刚好为sizeof(X)的对象而设计的。所以对于以下情况,如果是例如上面所写的operator new,则会不合理:

class Base
{
public:
	static void* operator new(std::size_t size) throw(std::bad_alloc);
	...
};

//假设类未声明operator new,则会继承基类的operator new
class Derived : public Base
{
	...
};

Derived* p = new Derived; //调用基类的operator new,但基类专属的operator new并非用来对付上述这种大小的对象
         

             而处理这种情况。一种解决方案是:将“内存申请量错误”的调用行为改用标准operator new,如下:

//解决方案
void* Base::operator new(std::size_t size) throw(sts::bad_alloc)
{
	if (size != sizeof(Base))   //如果大小错误,则调用标准的operator new,这里省略了对0的判断,因为sizeof(类)不会返回0
		return ::operator new(size);
	...
}
          这里没有判断是否为0, 因为C++规定sizeof(Base)无论如何不会为0.

           如果要写operator new[] :唯一要做的就是分配一块未加工内存。因为1无法计算这个类将含多少元素。且不知道对象大小,因为基类的operator new可能被继承。

          如果要写operator delete, 需要记住的唯一事情就是C++保证“删除null指针永远安全”,所以需要处理这种情况,如下伪码:

//定制operator delete
void operator delete(void* rawMemory) throw()
{
	if (rawMemory == 0) //如果被删除的是个null指针,则什么都不做
		return;
	以下归还rawMemory内存
}

//operator delete类版本
void Base::operator delete(void* rawMemory, std::size_t size) throw()
{
	if (rawMemory == 0)  //如果为null,什么都不做
		return;
	if (size != sizeof(Base))  //如果大小错误,调用标准版
	{
		::operator delete(rawMemory);
		return;
	}

	现在,归还rawMemory所指内存
	return;
}

         如果即将被删除的对象派生自某个base class而后欠缺virtual析构汉函数,则C++传给operator delete的size可能不正确。(不太明白),即如果你的基类遗漏virtual析构函数,则operator delete可能无法正确工作。

         总结:

        operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler,.它也应该有能力处理0 bytes申请,类专属版本还应该能处理“比正确大小更大的(错误)申请”。

       operator delete应该在收到null指针时不做任何事。类专属版本则还应处理“比正确大小更大的(错误)申请”。    

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

         当你写一个placement operator new,请确定也写了对应的placement operator delete。如果没有这么做,程序可能会发生隐微而时断时续的内存泄漏。

         当你声明placement new与placement delete,请确定不要无意识的遮掩了它们的正常版本。

         Widget* pw = new Widget;

         以上会有两个函数被调用,一个是用以分配内存的operator new,一个是Widget的default构造函数。假设第一个函数调用成功,第二个函数抛出异常,如果类内没有自定义的operator new, 即是标准的,则C++会保证将这个内存回收。即如果你只使用正常形式的new和delete, 运行期系统毫无问题可以找出那个"知道如何取消new所做所为,并恢复旧观"的delete.然而如果你声明了一个非正常形式的operator new,即带有附加参数的,则为了防止内存泄漏,需要提供对应形式的delete.

          placement new: 广义是指除了一定会有的size_t参数外还有其他参数的operator new,而众多的placement new版本中有一个特别有用的是“接受一个指针指向对象该被构造之处”,即声明如下:    

//一般所指的placement new
void* operator new(std::size_t, void* pMemory) throw();  
         这个版本的new已纳入C++标准程序库,它的用途之一是负责在vector的未使用空间上创建对象。也可以称之为一个特定位置的new.

         如以下类提供了一个类专属的operator new, 接受一个ostream, 记录相关分配信息,要保证不产生以上所写的内存泄漏,需要写成以下形式,下面是一个placement new:

//防止构造函数期间的内存泄漏
class Widget
{
public:
	...
	//一个placement new
	static void* operator new(std::size_t size, std::ostream& logStream)
	    throw(std::bad_alloc);
	//提供placement delete,防止operator new成功,构造函数失败时造成的内存泄漏
	static void* operator delete(std::size_t size, std::ostream& logStream)
	    throw();
	//operator new正常运行时调用的delete
	static void* operator delete(void* pMemory, std::size_t size)
		throw();
};

//以下语句如果引发Widget构造函数抛出异常,对应的placement delete会被调用,不会发生内存泄漏
Widget* pw = new (std::cerr) Widget;

//如果没有抛出异常,客户端有如下代码,则会调用正常的delete
delete pw;

         如上注释所写,placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。对一个指针施行delete,绝不会调用该函数。
         所以为了队所有与placement new相关的内存泄漏宣战,必须同时提供一个正常的operator delete(用于构造函数期间无任何异常被抛出)和一个placement delete(用于构造期间有异常被抛出)。而后者的额外参数必须和operator new一样。

         

        以上会出现的一个问题是成员函数的new会掩盖外围的new,即类专属的new会掩盖正常版本的news.假设有一个基类,其中声明一个placement operator new,客户端会无法使用正常形式的new, 同样derived class中的operator news会掩盖global版本和继承而来的operator new版本。遮盖是相对于new这个对应的类,这个对应的类只能使用自己定义的new, 不能再使用全局的了,与别的类无关,别的类还正常。如下:

//发生函数隐藏
class Base
{
public:
	...
	static void* operator new(std::size_t size, std::ostream& logStream)
	    throw (std::bad_alloc);   //会掩盖正常的global形式
	...
};

Base* pb = new Base;   //错误!因为正常的operator new被掩盖
Base* pb = new (std::cerr) Base;   //正确,调用Base的placement new

//继承类掩盖全局与基类的new
class Derived : public Base
{
public:
	...
	//重新声明正常形式的new
	static void* operator new(std::size_t size) throw (std::bad_alloc);
    ...
};

Derived* pb = new (std::clog) Derived; //错误!因为被掩盖了
Derived* pb = new Derived;  //正确,调用derived的new

          首先需要知道的是缺省情况下C++在全局作用域内提供以下三种形式的operator new:

//std中的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
        如果在类中声明了任何operator new, 则它会烟感上述这些标准形式,除非你自己的意愿就是组织class的客户使用这种形式,否则要确保他们在你所生成的任何定制型operator new之外还可以使用。而且对于每一个可用的operator new也应提供对应的operator delete. 如果要一且表现正常,则需要令你的类专属版本调用global版本。一个解决方法是建立一个基类,内含所有正常形式的new和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); //调用正常的new
	}
	static void* operator delete(void* pMemory) throw()
	{
		return ::operator delete(pMemory); //调用正常的delete
	}

	//placement new/delete
    void* operator new(std::size_t size, void* ptr) throw()
	{
        return ::operator new(size, ptr);
	}
    void* operator delete(void* pMemory, void* ptr) throw()
	{
        return ::operator delete(pMemory, ptr);
	}

	//nothrow new/delete
    void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
	{
		return ::operator new(size, nt)
	}
    void* operator delete(void* pMemory, const std::nothrow_t& nt) throw()
	{
		return ::operator new(pMemroy);
	}
};

       如果想以自定义形式扩充标准形式,则可利用继承机制及using声明式取得标准形式,就可以在自定义的new函数中调用global版本了:

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

	//添加自定义的placement new/delete,这样就可以在这些函数中调用标准形式的了
	static void* operator new(std::size_t size, std::ostream& logStream)
	    throw(std::bad_alloc);
	static void* operator delete(std::size_t size, std::ostream& logStream)
	    throw();
};



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值