1.对于C++而言,malloc是函数,但是new是运算符
看似函数和运算符实现的功能都差不多。但是对于C++来说,new是运算符就意味着我们可以进行运算符重载,这就意味着我们可以定制我们自己的new内存分配器。
同时,由于C++特有的异常处理机制,我们不但可以在我们内存分配失败的时候,new返回一个null,同时也可以报出一个bad_alloc错误,同时调用我们的new_handler(new运算符错误处理程序),但是我们的new_handler应该如何写呢。
还是先来一段代码:
class NewHandlerHolder
{
public:
explicit NewHandlerHolder(std::new_handler nh):handler(nh) //取得目前的new-handler.释放它
~NewHandlerHolder()
{std::set_new_handler(handler); }
private:
std::new_handler handler; //记录下来,阻止copying
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler)); //安装Widget的new-handler.
return ::operator new(size); //分配内存或抛出异常.恢复global new-handler.
}
template<typename T> //"mixin"风格的base class,用以支持
//class 专属的set_new_handler
class NewHandlerSupport
{
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 currentHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler=currentHandler;
currentHandler=p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
//以下将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler=0;
上面的代码,将我们重载的New以及new_handler函数封装成了一个类使用。但是,这里要注意的是,如果我们多重继承这个NewHandler类,要看适不适合我们的类,否则将会出现错误。
2.对于C++而言,new运算符可以动态申请我们的一个对象的空间
这里,我还是以一个例子来说明new来动态申请我们的一个对象的空间会引发什么问题:
Example* em=new Example
我们申请一个Example类的对象的时候,一共做了两件事:
1.类似malloc的过程申请了一块空间。
2.调用Example类的构造函数。
但是如果在第二步的时候,程序抛出了异常。那么是不是代表着new的过程失败了,抛出一异常,返回了null。第一步申请的空间还是申请,但没有得到指针进行delete操作,这就造成了内存泄露。
这明显是我们不想看到的不可控的情况。
实际上解决这个问题的方法也有两种:
1.对内存块进行签名。
2.我们每一次new的过程中都要返回一个指向空间的指针就行了。
实际上,我们的C++的new库已经为我们实现了这个功能:
#include<new>
void* operator new(std::size_t,void* pMemory) throw()
那个pMemory就是指向我们空间的指针啦,这样即使出现刚刚的情况,我们也可以利用这个指针来进行返回。
还有一种是基于签名的方式,这里还是直接上代码:
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); //调用malloc取得内存。
if(!pMem) throw bad_alloc();
//将signature写入内存的最前段落和最后段落
//减一个int是为了能够写在这个int的地方写上签名
*(static_case<int*>(pMem))=signature;
*(reinterpret_case<int*>(static_case<Byte*>(pMem)+realSize-sizeof(int)))=signature;
//返回指针,指向恰位于第一个signature之后的内存位置.
return static_cast<Byte*>(pMem)+sizeof(int);
}
可以从代码看出,主要做的改动就是把原来申请的空间扩展了两个int的签名,然后头尾加上签名,之后返回的是第一个签名后的内存位置。
但是这样做可能无法实现的内存对齐的要求(内存对齐是为了更快的访问(直接通过内存的移位操作进行访问))。
既然提到了内存对齐的问题,这里就要提一下一个优秀的内存分配器应该考虑什么,我们考虑个性化的内存分配器应该考虑什么?
3.优秀的内存分配器应该如何设计考虑
内存分配器的具体设计在我的另一篇博客上有提到:
http://blog.csdn.net/github_33873969/article/details/78571868
这里我们更专注内存分配器的其他方面:
1.线程安全(在多线程环境下,我们的new过程还是否安全)
2.文件系统的内存对齐问题
3.对于使用内存情况的统计问题
4.为了将相关对象成簇集中
这里我们简单的提几点
1.为了保证线程安全的时候,我们在new的时候如果对链表进行操作需要对链表的结点利用锁机制进行保护。但是这样就会降低一定量的效果。这就给我们定制new做了一个要求,要在只有单线程的环境下把相关锁关掉。
3.关于使用内存情况的统计问题,我们可以在我们的new的过程中,统计我们申请的块的大小情况,回收与申请的时间间隔如何,他们更倾向于FIFO次序或LIFO(后进先出)次序,之后我们可以再进行合理化定制。
4.为了将相关对象成簇集中。为了考虑局部性原理,我们可以将相同的对象放在一起,为了能够得到cache亲和,来进行快速访问。
Reference:Effective C++ (改善程序与设计的55个具体做法)