《Effective C++》 总结篇(定制new和delete)

《Effective C++》 总结篇(定制new和delete),第1张

本章主要唠嗑一下new和delete。

条款四十九:了解new-handle的行为

当operator new抛出异常以反应未获满足的内存需求之前会先调用new-handle。客户可以指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler。(在标准库)

#include 
using namespace std;

const int SIZE = 1024*1024;
char* buffer = NULL;

//没有内存了,set_new_handler(0),不再反复调用new-handler 
void outOfMemHandler()
{
	//2.安装另一个new - hander。
	cout << "Out of memory" << endl;
	//3.卸除new - handle
	set_new_handler(0);
}

//释放一些内存,确保下次能够分配内存  
void newHandlerFree()
{
	if (NULL != buffer)
	{
		//1.让更多内存被使用。
		cout << "Free some memory(buffer)" << endl;
		delete buffer;
		buffer = NULL;
		return;
	}
	cout << "No more memory for free" << endl;
	set_new_handler(outOfMemHandler);
}

int main()
{
	//buffer将会在后续内存分配失败后释放
	//这里buffer的size随着不同的环境调整,主要是为了制造没有足够内存分配
	buffer = new char[SIZE * 4999];

	set_new_handler(newHandlerFree);
	try
	{
		while (true)
		{
			new char[SIZE * 4999];
			cout << "Memory allocation successful" << endl;
		}
	}
	catch (const std::bad_alloc& e)
	{
		//5.抛出bad - alloc。
		cout << "Catch exception: " << e.what() << endl;
		//6.不返回
		abort();
	}
}

本例代码就是不断生成内存直到用尽,而抛出异常。为了防止当operator无法满足内存申请时反复调用new-handler函数,我们需要做以下事情:
1.让更多内存被使用。(调用new-handler之后将资源释放)
2.安装另一个new-hander。(当前的new-hander无法取得更多内存,则需要用另一个new-hander替代自己)
3.卸除new-handle(将null指针传给set_new_hander。使得抛出异常)
5.抛出bad-alloc。(这个异常不会被operator new捕捉,因此会被传播到内存索求处)
6.不返回。(用abort或exit)

C++不支持class专属的new-handers。
假设我们打算处理Widge内存分配失败的情况。
那么Widge的operator new应该做这些事情:
1.调用标准的set_new_hander,告知Widge的错误处理函数。这会将Widge的new-handler安装为global new-handler。
2.调用global operator new,执行实际之内存分配。如果分配失败,global operator new 会调用Widge的new-handler,因为那个函数才刚被安装为global new-hander。如果global operator new最终无法分配足够内存,会抛出一个bad-alloc异常。在此情况下Wdige的operator new必须恢复原本的global new-hander,然后再传播该异常。为确保原本的new-hander总是能够被重新安装回去,Widge将global new-handler视为资源,运用资源管理器防止资源泄漏。
3.如果global operator new 能够分配足够一个Widge对象所用内存,那么会返回一个指针,指向分配所得。Widge析构函数会管理global new-handler,它会自动将Widget’s operator new被调用前的那个global new-handler恢复回来。

#include
#include

class NewHandlerHolder {
public:
	explicit NewHandlerHolder(std::new_handler nh) :handler(nh) {}	//取得目前的new_handler
	~NewHandlerHolder() { std::set_new_handler(handler); }			//释放它,回复原本的global-new-handler

private:
	std::new_handler handler;
	NewHandlerHolder(const NewHandlerHolder&);						//阻止copying
	NewHandlerHolder& operator=(const NewHandlerHolder&);
};

class Widget {
public:
	static std::new_handler set_new_handler(std::new_handler p) throw();
	static void* operator new(std::size_t size) throw(std::bad_alloc);

private:
	static std::new_handler currentHander;		//new_handler只是一个函数指针,typedef void (*new_handler)()
};

//static成员必须在class定义时之外被定义(除非它们是const而且是整数型,见条款2),所以需要这么写:

std::new_handler Widget::currentHander = 0;	//在class实现文件内初始化为null


//Widget内的set_new_handler函数会将它获得的指针存储起来,然后返回先前(在调用之前)存储的指针,这也正是标准版set_new_handler的作为
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
	std::new_handler oldHandler = currentHander;
	currentHander = p;
	return oldHandler;
}

void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
	NewHandlerHolder h(std::set_new_handler(currentHander));		//安装Widget的new_handler分配内存或者抛出异常
	return ::operator new(size);			//回复global new_handler,如果失败,首先恢复原本的global-new-handler,再传播异常,成功的话返回一个指针
}
void outOfMem()//函数声明,此函数在Widget对象分配失败时被调用
{
	std::cout << "Out of memory" << std::endl;
}
int main()
{

	Widget::set_new_handler(outOfMem);//设定outOfMem为Widget的new-handling函数

	Widget* pw1 = new Widget;//如果内存分配失败,调用outOfMem

	std::string* ps = new std::string;//如果内存分配失败,调用global new-handling函数(如果有的话)

	Widget::set_new_handler(0);//设定Widget专属的new-handling函数为null

	Widget* pw2 = new Widget;//如果内存分配失败,立刻抛出异常(class Widget并没有专属的new-hangling函数)
	return 0;
}

可以加入模板使得可以复用。

#include
#include
class NewHandlerHolder
{
public:
	explicit NewHandlerHolder(std::new_handler nh)
 :handler(nh) {
}
	~NewHandlerHolder() {
 std::set_new_handler(handler);
 }
private:
	std::new_handler handler;
	//阻止拷贝 *** 作
	NewHandlerHolder(const NewHandlerHolder&);
	NewHandlerHolder& operator=(const NewHandlerHolder&);
};

template<typename T>
class NewHandlerSupport {
public:
	static std::new_handler set_new_handler(std::new_handler p) throw()
	{
		std::new_handler oldHandler = currentHander;
		currentHander = p;
		return oldHandler;
	}
	static void* operator new(std::size_t size) throw(std::bad_alloc)
	{
		NewHandlerHolder h(std::set_new_handler(currentHander));		//调用标准的set_new_hander,告知Widge的错误处理函数。
		return ::operator new(size);			//调用global operator new,执行实际之内存分配。如果失败,首先恢复原本的global-new-handler,再传播异常,成功的话返回一个指针
	}
private:
	std::new_handler handler;
	static std::new_handler currentHander;		//new_handler只是一个函数指针,typedef void (*new_handler)()
};

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHander = 0;


class Widget:public NewHandlerSupport<Widget> {
public:
	Widget() {
 }
	~Widget() {
 }
};




void outOfMem()//函数声明,此函数在Widget对象分配失败时被调用
{
	std::cout << "Out of memory" << std::endl;
}
int main()
{

	Widget::set_new_handler(outOfMem);//设定outOfMem为Widget的new-handling函数

	Widget* pw1 = new Widget;//如果内存分配失败,调用outOfMem

	std::string* ps = new std::string;//如果内存分配失败,调用global new-handling函数(如果有的话)

	Widget::set_new_handler(0);//设定Widget专属的new-handling函数为null

	Widget* pw2 = new Widget;//如果内存分配失败,立刻抛出异常(class Widget并没有专属的new-hangling函数)
	return 0;
}

直至1993年,C++还要求operator new必须在无法分配足够内存的时候返回null。但现在则应该抛出bad_alloc异常,但C++委员会不想抛弃那些”侦测null”的族群,故引入了“nothrow形式”---------某种程度上是因为他们在new的使用场合用了nothrow对象。

#include
#include


class Widget {};

int main()
{
	Widget* pw1 = new Widget;//如果内存分配失败,抛出bad_alloc
	if (pw1 == 0)
	{
		//不可能运行这句
	}

	Widget* pw2 = new (std::nothrow)Widget;//如果内存分配失败,返回0
	if (pw2)
	{
		//这句会在内存分配失败的时候运行
	}
	return 0;
}

这个表达式发生了什么?
nothrow版的operator new 被调用,用以分配足够内存给Widge对象。如果分配失败便返回null指针。如果分配成功,则Widge的构造函数会被调用。但其实构造函数可能也会抛出异常,所以nothrow new不能保证operator new不抛异常。因此完全不需要用nothrow

请记住:
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常的。

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

为什么有人想替换编译器提供的new和delete?因为:

  1. 用来检测运用上的错误。方便log日志之类的,查看new分配的地址。
  2. 为了强化效能。可以自己定制new/delete以加快性能和内存使用。
  3. 为了收集使用上的统计数据。收集某些信息,比如分配区块大小分布,寿命分布,分配次序等。

我们可以简单的写一个自己的new

#include
#include
static const int signture = 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);//增加大小使得能塞入两个sintrue

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

	//写入内存的最前段落和最后段落
	*(static_cast<int*>(pMem)) = signture;
	*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)) + realSize - sizeof(int)) = signture;
	/*signture +   Byte*  +  signture */
	//返回指针,恰好位于第一个signture之后的内存位置
	return static_cast<Byte*>(pMem) + sizeof(int);
}

但这个函数有很多缺点:

  1. 并没有反复调用某个new-handing函数的机制
  2. 没有满足齐位。某些地方要求指针地址是4倍数或doubles的地址必须是8倍数。

本条款的主题是,了解何时可在”全局性的”或“class专属的”基础上合理替换缺省new和delete。挖掘更多细节之前,我先对答案进行一些摘要:

  1. 为了检测运用错误
  2. 为了收集动态分配内存之使用统计信息
  3. 为了增加分配和归还的速度。
  4. 为了降低缺省内存管理器带来的空间额外开销。
  5. 为了弥补缺省分配器中的非最佳齐位。
  6. 为了将相关对象成簇集中
  7. 为了获得非传统的行为

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

条款五十一:编写new和delete时需固守常规

我们需要遵守的规则:

  1. 内存不足的时候必须调用new-handing函数
  2. C++规定即使用户要求0bytes,operator new也得返回一个合法的指针。一般是把0byte视为1byte
  3. 将大小有误的释放行为,转交给标准的delete。
  4. operator new[]唯一需要做的一件事就是分配一块未加工内存.
  5. operator delete应该在收到null指针时,不做任何事.class专属版本则还应该处理"比正确大小更大的错误申请"

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

条款五十二:写了placement new也要写placement delete
  1. 如果operator new接受的参数除了一定会有的那个size_t之外,还有其他参数,这便是个所谓的placement new.
  2. 相应的placement delete指"参数个数和类型都与operator new相同的"operator delete
  3. 运行期,若replacement new抛出异常,则系统会去找与placement new相对应的placement delete版本,若找不到则什么也不做.
    但是placement delete只有在"伴随placement new调用而触发的构造函数"出现异常时才会被编译器自动调用.对一个指针显式调用delete不会导致调用placement delete.
  4. 缺省情况下,C++在global作用域提供三种形式的operatornew
    * 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
  5. 当声明了专有类的new和delete之后,请注意,它会遮掩std的标准new和delete.
  6. 为了避免class的专属news掩盖其他的news。我们可以使用继承。在base class中包含所有正常形式的new和delete。凡是想要以自定义扩充标准形式的客户可以利用继承机制和using声明式来取得标准形式。
class StandardNewDeleteForms{
public:
	static void* operator new(std::size_t size)noexcept(std::bad_alloc)
	{ return ::operator new(size); }	//交给global new处理
	static void operator delete(void* pMemory)noexcept
	{ ::operator delete(pMemory); }
	...
	//因为标准new与delete有三个版本 normal new/delete,placement new/delete, nothrow new/delete,这边就不写了
	
};

class Widget:public StandaedNewDeleteForms{
public:
	using StandardNewDeleteForm::operator new;		//让标准形式可见
	using StandardNewDeleteForm::operator delete;
	...
};

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

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1324361.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存