本章讨论的new与delete适用与new[ ]与delete[ ]。
还有STL容器使用的heap内存是容器所拥有的分配器对象管理的。不是被new和delete直接管理。
了解new-handler的行为
当operate new无法满足某一内存的分配需求的时候,就会抛出异常。
但在抛出异常之前,他会调用一个客户指定的处理函数,new-handler,客户为了指定这个函数,就要使用set_new_handler,
void outOfMan()
{...}
int main(){
std::set_new_handler(outOfMan);
int* PP = new int[1000000000]; //此时会调用outOfMan
};
一个设计良好的new-handler必须做以下的事情:
1、让更多的内存可被使用
2、安装另一个new-handler(如果这个new-handler无法取得更多内存的话)
3、卸载new-handler(将null传给set_new_handler)
4、抛出bad_alloc的异常
5、不返回(调用abort或exit)
可能会希望以不同的方式处理不同的内存分配失败的问题。
让不同的类有专属的new-handler。
假设你是要处理Widget class的内存分配失败的情况:
class Widget{
public:
static std::handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size) noexcept(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
std::new_handler Widget::currentHandler = 0; //static成员(除非是const而且是整数型)需要在class定义式外被定义。
std::new_handler Widget::set_new_handler(std::new_handler p) noexcept{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
Widget的operate new做的事情:
1、调用标准的set_new_handler,告知Widget的错误处理函数。这个会将new-handler安装为global new-handler。
2、调用global operator new,进行内存分配。分配失败后会调用Widget的new-handler(刚才已经被安装成global new-handler了),如果global operator new 无法分配足够的内存,就会抛出bad_alloc异常。然后Widget的operator new恢复原本的global new-handler,再传播该异常。为了确保new-handler总是能被重新安装,Widget用资源管理对象(RAII)来管理global new-handler防止资源泄漏。
3、如果global operator new 能分配对象,就会返回一个指针,指向分配所得。
用C++描述的话:
//RAII类
class NewHandlerHolder{
public:
explicit NewHandlerHolder(std::new_handler nh): handler(nh)
{}; //取得目前的new-handler
~NewHandlerHolder()
{ std::set_new_handler(handler); } //释放
private:
std::new_handler handler; //记录
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&); //阻止copying
};
void* Widget::operator new(std::size_t size) noexcept(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler)); //安装Widget的new-handler
return ::operator new(size); //分配内存或抛出异常的时候返回 global new-handler
}
//Widget的客户应该类似于这样子使用其new-handler
void outOfMem(); //此函数再Widget分配失败的时候调用
Widget::set_new_handler(outOfMem); //设定outOfMem为Widget的new-handler函数
Widget* pw1 = new Widget; //如果内存分配失败,调用outOfMem
std::string* ps = new std::string; //如果内存分配失败,调用global new-handing函数
Widget::set_new_handler(0); //设置Widget的专属函数为null
Widget* pw2 = new Widget; //内存分配失败直接抛出异常
实现这些方案的代码并不因为class的不同而不同,我们可以利用创建一个base class来允许derived class继承特定能力(new-handler)。然后把base class转化为template,如此以来每个derived class将获得实体互异的class data。
设计的目的是base class 部分让derived class继承他们所需要的set_new_handler和operator new。使用template的原因是让每个derived class 获得一个实体互异的currentHandler成员变量。代码于上面的代码很近似。
template<typename T>
class NewHandlerSupport{
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size)noexcept(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p)noexcept
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size)noexcept(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;
//向Widget中添加set_new_handler就相当轻而易举了,只需要Widget继承NewHandlerSupport<Widget>即可
class Widget: public NewHandlerSupport<Widget>
{
...//不需要声明set_new_handler或是operator new
};
上述的代码中,TMP未使用类型T,因为我们只是希望,继承newHandlerSupport的每一个class,拥有实体互异的NewHandlerSupport复件(不同的currentHandler),T只是用来区分不同的derived class。
然后,有一点相当的怪。就是Widget继承一个类型是他本身的template。但这是一个有用的技术,叫做CRTP。
C++还有提供一种new分配失败就返回null的形式,nothrow new对于只能适用于内存分配,后继的构造函数还是可能抛出异常。
了解new和delete的合理替换时机
要替换编译器提供的operator new与operator delete原因:
1、用来检测运用上的错误。如果operator new持有一个动态分配的地址,而operator delete移除这个地址,这样可以很容易的检查出是否有内存泄漏或是多次delete现象。在operator new中在内存空间放置特定的签名来检查内存分配区是否因为编码错误导致overrun与underrun。
2、强化效能。编译器自带的operator new与operator delete主要用于一般目的,定制版本的可以在特殊的内存分配上有很大的提升。
3、收集使用上的统计数据。
4、增加分配和归还的速度
5、降低缺省内存管理器带来的空间额外开销
6、弥补缺省分配器中的非最佳齐位
7、将相关对象集中
8、获得非传统的行为
//这个代码其实不完善,new里面应该要包含一个无穷循环,在下一章有解释
static const int signature = 0xDEAAAA;
typedef unsigned char Byte;
void* operator new(std::size_t size) noexcept(std::alloc)
{
using namespace std;
//增加内存大小,使的可以加入signatures
size_t realSize = size + 2 * sizeof(int);
//调用malloc获取内存
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;
//返回内存位置
return static_cast<Byte*>(pMem) + sizeof(int);
}
如果定制new的时候要注意齐位的概念,一个未有适当的齐位的指针,会导致程序崩溃与变慢。
编写new和delete的时候需要固守常规
operator new其实不止一次尝试分配内存,并在每次失败之后调用new-handling函数。
//operator new的伪码
void* operator new(std::size_t size)noexcept(std::bad_alloc)
{
using namespace std;
if(size == 0)
{
size = 1;
}
while(true)
{
-尝试分配size bytes
if(分配成功)
return 一个指向分配得来的指针
//如果分配失败,找出目前的new-handling函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)
(*globalHandler)();
else
throw std::bad_alloc();
}
}
实现一个 operator new需要返回一个正确的值,内存不足的时候必须调用new-handling函数,必须要有对于0内存需求的准备,还需要避免不慎掩盖正常形式的new。
operator new里面含有一个while循环,而退出这个循环的唯一方式是,内存被成功分配或是new-handler作出了一个描述与上面一个条款的事情:让更多内存可用之类的。
operator new成员函数会被derived class继承,会导致很有趣的事情。derived class如果没有声明operator new,但是可以调用base class的operator new来分配内存。但base class并不是用来应对这种情况的。
void* Base::operator new(std::size_t size) noexcept(std::bad_alloc)
{
if(size != sizeof(Base)) //如果大小不符合,令标准operator new处理
{
return ::operator new(size);
}
}
但对于array new来说,就是operator new[ ] ,因为你不能知道每个对象有多大(operator new[ ] 可能是继承调用,derived 对象通常比base 对象大),我们不能假设array的每个元素大小与个数,此外还需要知道operator new [ ]的size_t参数可能会比被填以对象的内存数量更多,因为动态分配的array可能包含额外空间来存放元素个数。所以我们唯一需要做的一件事情是分配一个为加工的内存。
对于delete来说,C++保证delete删除null指针永远安全。
//non-member版本
void operator delete(void* rawMemory) noexcept
{
if(rawMemory == 0)
return;
}
//member版本
class Base{
public:
static void* operator new(std::size_t size) noexcept(std::bad_alloc);
static void* operator delete(void* rawMemory,std::size_t) noexcept;
};
void Base::operator delete(void* rawMemory,std::size_t size) noexcept
{
if(rawMemory == 0) //检查null指针
return;
if(size != sizeof(Base)) //检查是否大小错误
{
::operator delete(rawMemory); //交给标准版处理
return;
}
return;
}
同条款7的一个理由:如果base class缺少virtual 析构函数的话,C++传给operate delete的size_t数值可能不对,这也是一个base class拥有virtual析构函数的一个理由。
写了placement new也要写placement delete
如果operator new接受的参数除了size_t还有其他的参数,那么就是个placement new。
最出名的placement new是接受一个指针指向对象该被构造之处:
void* operator new(std::size_t, void* pMemory) noexcept;
//已经被添加到标准库,你只需要在#include <new>中就可以取用他
在new一个对象的过程中,需要调用构造函数与operator new。如果operator new调用成功但是构造函数调用失败的话,运行期系统就会寻找与之匹配的operator delete来释放获取的内存。
如果你是使用placement new的话,就必须构建一个相同的placement delete,如果没有的话,运行期系统就不知道找哪个delete,内存泄漏。
但是注意,placement delete只有在伴随着placement new调用而触发的构造函数这种情况下才会调用,所以我们还是必须提供一个正常的delete。
class Widget{
public:
static void* operator new(std::size_t size,std::ostream& logStream) noexpect; //placement new
static void operator delete(void* pMemory)noexcept; //正常的delete
static void operator delete(void* pMenory,std::ostream& logStream) noexcept; //placement delete
};
为了避免class的专属news掩盖其他的news。我们可以使用继承。在base class中包含所有正常形式的new和3delete。
凡是想要以自定义扩充标准形式的客户可以利用继承机制和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;
...
};