C++ new和delete的定制化设计

一、了解new-handle的行为

1.new_handler的声明。当operator new抛出异常以反映一个未获满足的内存需求之前,会先调用客户制定的错误处理函数,所谓的new-handler。

namespace std{
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

new_handler是一个typedef,定义出一个指针指向函数。set_new_handler函数是一个传入参数为new_handler,返回为new_handler的函数,后面的throw是一份异常明细,表示该函数不抛出任何异常。

set_new_handler方法的简单使用过程,在无法分配足够空间时,会调用我们指定的函数。

void outOfMem(){
    std::cerr << "Unable to satisfy request for memory\n";
    std::abort();
}
int main(){
    std::set_new_handler(outOfMem);
    int* pBigDataArray = new int[10000000000L];
}

2.当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。

因此在设计new-handler函数时,需要遵循以下规则

(1)让更多内存可被使用。一个方法是一开始程序就分配了一大块内存,相当于自己预留了一块,然后在new-handler第一被调用的时候,将其释放一部分。

(2)安装另一个new-handler。修改调用的new-handler使其能够成功分配内存。

(3)卸除new-handle。将null指针传给set_new_handler,没有安装new-handler时,operator new会在内存分配不成功时抛出异常。

(4)抛出bad_alloc(或派生自bad_alloc)异常。这样的异常不会被operator new捕捉,也就不会继续调用new-handler函数,能够传播到内存所求处。

(5)不返回。直接调用abort或exit。

3.为了设计针对不同类的动态内存分配使用不同的new-handler函数,针对不同类,声明自己的set_new_handler和new操作。set_new_handler方法通常是将旧指针保存并返回的操作,标准该函数也是这样做的。

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 currentHandler;
};

std::new_handler set_new_handler(std::new_handler p) throw(){
	std::new_handler oldHandler = currentHandler;
    currentHandler  = p;
    return oldHandler;
}

Widget的opearator new会做以下事情:

(1)调用标准set_new_handler,告知Widget的错误处理函数。这会将Widget的new-handler安装为global new-handler,也就用类里的new-handler进行安装。

(2)调用global operator new,执行实际的内存分配。如果分配失败,会调用Widget的new-handler,如果最终还是无法分配足够内存,会抛出一个bad_alloc的异常。在最后抛出bad_alloc异常时,会让Widget的operator new恢复原本的global new-handler,也就是复原global new-handler。为确保new-handler总是能够被重新安装回去,通常将其视为资源,运用资源管理对象进行管理。

(3)如果分配成功,Widget会返回一个指针,指向分配所得。Widget的析构函数会管理global new-handler,将之前的global new-handler恢复过来。

资源处理类NewHandlerHolder:

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&);
}

Widget的operator new:

void* Widget::operator new(std::size_t size) throw(std::bad_alloc){
    NewHandlerHolder h(std::set_new_handler(currentHasndler));	//	安装新的new-handler
    return ::operator new(size);	//分配内存或抛出异常,因为h是local变量,所以在函数退出后,自动进行析构函数,也就恢复global new-handler
}

Widget客户使用:

void outOfMem();

Widget::set_new_handler(outOfMem);
Widget* pw1 = new std::string;
Widget::set_new_handler(0);
Widget* pw2 = new Widget;

4.new_handler使用的模板形式,需要在类前事先初始化currentHandler。以下代码中虽然未用到模板T,但是针对我们每一个T都会生成一份currenHandler,我们的主要目的就是继承NewHandlerSupport的类能够有一份实体互异的NewHandlerSupport复件。

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;	//	不然就无定义
template<typename T>
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(currentHasndler));	//	安装新的new-handler
    return ::operator new(size);	//分配内存或抛出异常,因为h是local变量,所以在函数退出后,自动进行析构函数,也就恢复global new-handler
}
class Widget: public NewHandlerSupport<Widget>{
    ...
};

5.nothrow方法,用于在抛出异常时,返回0。nothrow只保证operator new不出现异常,不保证构造函数不出现异常(不用他)。

class Widget{...};
Widget* pw1 = new Widget;	//	抛出bad_alloc
if(pw1 == 0)...	
    Widget* pw2 = new (std::nothrow) Widget;	//	pw2 = 0
if(pw2 ==0 )...

二、了解new和delete的合理替换时机

1.替换编译器提供的operator new或operator delete常见三种理由

(1)用来检测运用上的错误。

  • 将new所得内存delete掉却不幸失败,会导致内存泄露。
  • 在new所得内存身上多次delete会导致不明确行为。
  • 各种编程错误可能导致数据“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块起点之前)。

简单检测方法,可以自行定义一个operator news,超额分配内存,以额外空间来放置这段内容的byte patterns(签名)。我们在delete的时候可以检查该签名是否发生改变,如果改变,则说明在分配区的某个生命时间点发生了overrun或underrun,operator delete可以记录那个事实。

(2)可以强化性能。

  • 编译器所带的operator new和operator delete主要用于一般目的,采取的中庸之道,需要保证不但可被长时间执行的程序接受,也可被执行时间少于1秒的程序接受。必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。需要考虑破碎的问题,也就是程序无法满足大区块内存要求,即使此时有总量足够但分散为许多小区快的自由内存。
  • 定制版的operator new和operator delete性能胜过默认的版本。

(3)为了收集使用上的统计数据。

  • 时刻收集软件是如何使用动态内存,大小分布、寿命分布、倾向于FIFO还是LIFO、运用形态是否随时间改变、任何时刻所使用的最大动态分配量(高水位)是多少,自行定义operator new和operator delete可以更轻松的收集这些信息。

2.定制型operator new的方式

(1)初阶段global operator new,促进并协助检测“overruns”或“underruns”。

static const int signature = OxDEADBEEF;
typedef unsigned char Byte;
void* operator new(std::size_t) throw(std::bad_alloc){
    using namespace std;
    size_t realSize = size + 2 * sizeof(int);
    
    void* pMem = malloc(realSize);
    if(!pMem) throw bad_alloc();
    
    *(static_cast<int*>(pMem)) = signature;	//	static_cast用于自然和低风险类型的转换,不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。
    *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature;	//	reinterpret_cast执行的用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。作用是对其进行重新诠释,也就是并不会改变括号中运算对象的值,而是对该对象从位模式上进行重新解释
    return static_cast<Byte*>(pMem) + sizeof(int);
}

以上方法中忽略了反复调用某个new-handling。

以上方法忽略了内存对齐的概念,通过malloc获得的内存空间是自动对齐的,但是以上方法中返回的是得自malloc的但是便宜了int大小的指针,不安全。

解决方法:

(1)自己调试

(2)使用某些商业产品编译器自带的内存管理器消除自行撰写new和delete的需要

(3)选择开放源码中的内存管理器,必须Boost程序库的Pool等

2.何时可在“全局性的”或“class专属的”基础上合理替换缺省的new和delete。

(1)检测运用错误

(2)收集动态分配内存之使用统计信息

(3)增加分配和归还的速度

(4)为了降低缺省内存管理器带来的空间额外消耗

(5)为了弥补缺省分配器中的非最佳齐位

(6)为了将相关对象成簇集中。某些对象往往一起被使用,为了减少内存页错误,可以将其集中在尽可能少的内存页

(7)为了获得非传统的行为。比如说delete的时候不是释放空间,而是全部覆写为0。

三、编写new和delete时需要固守常规

1.处理0byte内存空间请求与反复调用new-handling问题

void* operator new(std::size_t) throw(std::bad_alloc){
    using namespace std;
    if(size == 0){
        size = 1;	//	当指明分配0byte内存空间时,分配1内存空间
    }
    while(true){
        尝试分配size bytes;
        if(分配成功)
        return (一个指针,指向分配得来的内存)
        
        //	分配失败
        new_handler globalHandler = set_new_handler(0);
        set_new_handler(globalHandler);
        
        if(globalHandler)	(*globalHandler)();
        else throw std::bad_alloc();
         
    }
}

2.为某些类设计的operator new成员函数会被derived class继承,一般这种成员函数都是单独为该类设计,大小为sizeof(x),允许derived类对象,可能会造成其他的复杂性。

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

在以上代码中p使用的new方法依然用的Base类的new方法。

修改代码:

void* operator new(std::size_t size) throw(std::bad_alloc){
    if(size != sizeof(Base))	//	如果大小错误,说明不是该类方法在调用new,包含了分配内存空间为0的情况
        return ::operator new(size);	//	调用标准的operator new方法
    ...	//否则进行以下代码
}

3.在对array类对象进行空间分配时,其对象个数不能假设为申请大小/sizeof(Base),因为array类除了需要有每个对象的存储空间外,还有类似size_t等参数需要存储空间。

在delete的时候需要考虑删除null指针的时候,以下时non-member成员函数

void operator delete(void* rawMemory) throw(){
    if(rawMemory == 0) return;	// 删除空指针时,什么都不做
    
    ...
}

在member成员函数中,也需要考虑derived类的情况

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();
    
    ...
};
class Derived: public Base{
    ...
}
void Base::operator delete(void* rawMemory, std::size_t size) throw(){
    if(rawMemory == 0)	return ;
    if(size != sizeof(Base)){
        ::operator delete(rawMemory);
        return
    }
    归还rawMemory所指的内存;
    return;
}

四、写了placement new也要写placement delete

1.在new表达式中,会调用两个函数,第一个是用于分配内存的operator new,一个是Widget的default构造函数。

Widget* pw = new Widget;

第一个函数调用成功,第二个函数调用失败时,需要恢复第一个函数申请的资源,否则会造成内存泄露。在Widget构造函数出现异常的时候,pw并未被赋值,所以用户没有指针指向该被归还的内存,通常恢复旧观的责任落在C++运行期系统上。

正常的operator new:

void* operator new(std::size_t) throw(std::bad_alloc);

对应的operator delete:

void operator delete(void* rawMemory) throw();
void operator delete(void* rawMemory, std::size_t size) throw();

一般系统会调用相应的operator delete。当使用特指的new方法时,运行期系统无法找出取消new所作所为的delete函数

2.placement new是说非正常形式的new方法,常用的如下:

void* operator new(std::size_t, void* pMemory) throw();

在类中声明非正常形式的new方法时,为了让C++运行期系统找到对应的delete方法,我们在声明传统的delete方法的同时,需要加上和new方法相同的额外参数。

class Widget{
    public:
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
    static void operator delete(void* pMemory) throw();
    static void operator delete(void* pMemory, std::ostream& logStream) throw();
    ...
};
Widget* pw = new (std::cerr) Widget;	//内存不会再泄露

以上方法中使用的是placement new方法,因此当发生异常时,会调用对应的placement delete方法

但是当直接delete时,会调用非placement版本。

delete pw;

3.在类中声明的operator new方法会掩盖客户期待的其他new方法,比如正常的new方法。以及derived类的new方法会遮掩Base类方法的new方法,因此我们通常在设计类的时候,需要把所有类型的new方法以及对应delete方法。

class Widget{
    public:
    // 正常版本
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
    static void operator delete(void* pMemory) throw();
    
    //	placement版本
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
    static void operator delete(void* pMemory, std::ostream& logStream) throw();
    
    //	nothrow版本
    static void* operator new(std::size_t size, const std::nothrow_t& nt) throw();
    static void operator delete(void* pMemory, const std::nothrow_t& nt) throw();
    ...
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值