effective cpp 读书笔记49-52

定制new和delete

条款49.了解new-handler的行为

参考

https://blog.csdn.net/kangroger/article/details/44280793

https://blog.csdn.net/u011058765/article/details/46501195

当operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,即new-handler,为了指定这个用以处理内存不足的函数,客户必须调用set_new_handler,那是声明于里的标准程序库函数

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

new-handler是个函数指针,该函数没有参数也不返回任何东西。set_new_handler是设置一个new-handler并返回一个new-handler函数,返回的new-handler是指向set_new_handler被调用前正在执行的那个new-handler函数。后面的throw是一份异常明细,表示该函数不抛出异常

举个例子

void outOfMem()
{
    std::cerr<<"Unable to satisfy request for memoryn";
    std::abort();//终止该进程
}
int main()
{
   std::set_new_handler(outOfMem);
   int *pBigDataArray=new int[100000000L];
   ...
}

就本例而言,operator new无法为100000000个整数分配足够空间,outOfMem会被调用。程序在翻出一个消息后被终止

当operator new 无法满足内存申请时,它会不断调用new-handler函(具体引起不断调用的代码请看条款51)数,直到找到足够内存.就此,设计一个良好的new-handler函数必须做以下事情

  • 让更多内存被使用

这样可以造成operator new内的下一次内存分配动作可能成功。一个做法是,程序一开始就分配一大块内存,当new-handler第一次被调用时将它释放。

  • 安装另一个new-handler

当前的new-handler无法取得更多内存时,或许它直到哪个new-handler有此能力。

  • 卸除new-handler

即将null指针传给set_new_handler,一旦没有安装任何new-handler,operator new在内存分配不成功时便抛出异常。

  • 抛出bad_alloc(或派生自bad_alloc)的异常

这样的异常不会被operator new捕捉,因此不会被传播到内存索求处。

  • 不返回

直接调用abort或者exit

有时你希望以不同方式处理内存分配失败情况

class X{
public:
    static void outOfMemory();
    ...
};
class Y{
public:
    static void outOfMemory();
    ...
};
X* p1=new X;//分配不成功,调用X::outOfMemory
Y* p2=new Y;//分配不成功,调用Y::outOfMemory

只需要令每一个class提供自己的set_new_handler和 operator new即可,其中set_new_handler指定该class的new-handler,operator new 则确保再分配对象class内存的过程中以该class的new-handler来替换 global new-handler

举个例子

  class NewHandlerHolder{
    public:
        explicit NewHandlerHolder(std::new_handler nh)
        :handlere(nh){}
        ~NewHandlerHolder()
        { std::set_new_handler(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 currentHandler;
    };
    std::new_handler Widget::currentHandler=0;
    std::new_handler Widget::set_new_handler(std::new_handler p) throw()
    {
        std::new_handler oldHandler=currentHandler;
        currentHandler=p;
        reutrn oldHandler;
    }

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


    void outOfMem();
    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;//内存分配失败则立刻抛出异常

我们使用资源管理类NewHandlerHolder来保存替换前的内存处理函数,在构造函数取得目前的new-handler,在析构函数释放它。显然地,这个类禁止拷贝。

上述实现步骤:

首先,通过Widget::set_new_handler(outOfMem)语句设置Widget内的static std::new_handler currentHandler变量。然后,调用new Widget时,我们进入Widget::operator new(std::size_t size) throw(std::bad_alloc)函数体内,NewHandlerHolder h(std::set_new_handler(currentHandler));语句起到的作用如下

1.在定义一个NewHandlerHolder对象的时候将替换前的内存处理函数保存在该对象的std::new_handler handler变量中。

2.设置新的针对类Widget的new_handler函数。执行::operator new(size)进行内存分配,如果内存分配出错则调用刚替换的内存处理函数。在退出该函数体的时候,因为NewHandlerHolder的析构函数,还原内存处理函数,不影响以后别的内存分配异常的执行。

另一种设计使用了模板,让derived class来继承base class,base class定义了set_new_handler和opeartor new,template确保每一个derived class 获得一个实体互异的currentHandler变量,相比于上个版本,唯一真正以上的不同是,它现在可以被任何所需要的class使用

  class NewHandlerHolder{
public:
    explicit NewHandlerHolder(std::new_handler nh)
    :handlere(nh){}
    ~NewHandlerHolder()
    { std::set_new_handler(handler); }
private:
    std::new_handler handler;
    NewHandlerHolder&(const NewHandlerHolder&);//防止copying
    NewHandlerHolder& operator-(const NewHandlerHolder&);
};
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(currentHandler);
    return ::operator new(size);

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

class Widget:public NewHandlerSupport<Widget>{

};

只要令widget继承自NewHandlerSupprt就好。当我们调用语句class Widget:public NewHandlerSupport时,我们实例化了一个NewHandlerSupport类,我们的类Widget继承自该类,所以widget类中继承了NewHandlerSupport的函数,实际程序执行过程和第一种方式一样,唯一不同的地方就是类widget不用重新定义有关内存处理的静态函数。

旧版本的operator new分配失败返回null指针,不抛出异常,旧版本为nothrow形式

new(std::nothrow) T;

使用旧版本的operator new发生两件事,第一分配内存给Widget对象,如果失败返回null指针。第二,如果成功,调用Widget的构造函数,但是在这个构造函数做什么,nothrow new并不知情,有可能再次开辟内存。如果在构造函数使用operator new开辟内存,那么还是有可能抛出异常并传播。使用nothrow new只能保证operator new不抛出异常,不能保证像new(std::nothrow) Widget这样的表达式不抛出异常。所以,并没有运用nothrow的需要。

请记住

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时调用。

  • Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配,后继的构造函数还是可能抛出异常。、

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

替换编译器提供的operator new的三个理由:

  • 用来检测应用上的错误
    可能出现的错误:1.对new所得内存进行delete却不幸失败,会导致内存泄露,在new所得内存进行多次delete,会导致不确定行为。
    如果operator new持有一串动态内存分配所得地址,而operaotr delete 将地址从中划走,很容易检测出上述错误用法。我们定义一个operator news,超额分配内存,在额外的空间(写入区块之前和之后)放置特定的签名,使用operaotor delete便可以检查上述签名是否原封不动。如果在额外内存发生写操作,operator delete便可以志记那个指针

  • 为了强化效能

编译器所带的operaotor new 和operator delete 用于一般目的,它们可以被执行时间不同的程序接受,也可以接受不同大小的分配请求,包括大块内存,小块内存,大小混合型内存。这不会比专属的内存分配器更高效。

  • 为了收集使用上的数据

你的软件如何分配的大小分布如何?寿命分布如何?它们倾向于FIFO还是LIFO次序或随机次序进行分配?任何时刻使用的最大动态分配量是多少?等等,自行定义的operator new 和 operator delete 可以使我们轻易收集到这些信息。

看个例子

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);//增加大小,塞入两个sinature

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

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

    return static_cast<Byte*>(pMem)+sizeof(int);
}

暂且忽略之前所说的operator new内应该有个循环,反复调用new-handling

现在先讨论另外一个主题:对齐

许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如可能是指针的地址必须是4的倍数(four-byte aligned)或double的地址是8的倍数(eight-byte aligned)。没有这些约束可能会导致运行期硬件异常。有些体系结构要求没这么严格,没有字节对齐不会导运行效率低下。

C++要求所有operator new返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作。所以令operator new返回一个得自malloc的指针是安全的。但是上面实现中,我们偏移了一个int的大小,就不能保证其安全了。例如,如果返回double指针,就不是8字节对齐了。
像对齐这类技术细节,可以区分内存管理器的质量。写一个能够运行的内存管理器并不难,难的是让它总是能够高效优良的运作。一般来说,若非必要,不要去写内存管理器。

很多时候也是非必要的。有些编译器已经在它们的内存管理函数中切换至调试状态(enable debugging)和志记状态(logging)。许多平台上有商业产品可以代替编译器自带的内存管理器,可以用它们来提高机能和改善效率。

另外一个选择是开源领域中的内存管理器。它们对许多平台都可以用。Boost程序库(条款 55)的Pool就是这样的一个分配器,它对常见的分配大量小内存很有帮助。一些小型开源内存分配器大多都不完整,缺少移植、线程安全、对齐等考虑。

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

  • 为了增加分配和归还的速度。

使用定制的针对特定类型对象的分配器,可以提高效率。例如,Boost提供的Pool程序库便是。如果在单线程程序中,你的编译器所带的内存管理具备线程安全,你可以写个不具备线程安全的分配器而大幅度改善速度。

  • 为了降低缺省内存管理器带来的空间额外开销。

泛用型分配器往往(虽然并非总是)不只比定制型慢,还使用更多空间,因为它们常常在每一个分配区块上招引某些额外开销。针对小型对象开放的分配器,例如Boost库的Pool,本质上消除了这样的额外开销。

  • 为了弥补缺省分配器的非最佳对齐(suboptimal alignment)。

X86体系结构上的double访问最快–如果它们是8-byte对齐。但是编译器自带的operator new并不保证分配double是8-byte对齐。

  • 为了将相关对象成簇集中。

如果特定的某个数据结构往往被一起使用,我们希望在处理这些数据时将“内存页错误”(page faults)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这样就可以将它们成簇集中到尽可能少的内存也上。

  • 为了获得非传统的行为。

有时候我们需要做operator new和delete没做的事。例如,在归还内存时将其数据覆盖为0,以此增加应用程序的数据安全。

请记住

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

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

条款50告诉我们什么时候会写一个自己的operator new 和operator delete,本条款将告诉我们编写它们时该遵守的规则

下面是一个 non-member operator new 伪代码

void* operator new(std::size_t size) throw(std::bad_alloc)
{
    using namespace std;
    if(size==0){//处理0-byte申请
        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 函数做了意见描述于条款49的事:让更多内存可用,安装另一个new-handler,卸除new-handler,抛出bad_alloc异常,承认分配失败而直接return。

new_handler globalHandler=set_new_handler(0);
set_new_handler(globalHandler);

这段代码中,因为我们无法直接取得new-handling指针,必须调用set_new_handling(其返回这次调用的函数指针),在把它恢复原样。但在多线程环境中,你需要某种锁(lock)以便安全处理new-handling函数背后的(global)数据结构。

C++规定,即使客户要求0 byte,operator new也要返回一个合法指针。

涉及到继承时,面的operator new成员函数可能会被derived classes继承。注意分配内存大小size,它是函数接收的实参,条款50提到,定制内存分配器往往是为了特定的class对象,以此来优化,不是为了该class的derived classes。

class Base{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
};
class Derived:public Base
{...};//假设为重新定义operator new
Derived* p=new Derived;//这里调用了Base::operator new

如果是class专属的operator new,应该改为这样:

void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
    if(size!=sizeof(Base))
        return ::operator new(size);//使用标准的operator new
    ...
}

如果Base class 专属的operator new 并非被设计用来对付上述情况(实际上往往如此),处理此情势的最佳做法是将“内存申请量错误”行为改变为标准 operator new

如果你要实现class专属arrays内存分配,那么你要实现operator new[],我们无法知道这个array含有多少个元素对象。可能你不知道每个对象多大,因为base class的operator new[]有可能经由继承被调用,将内存分配给derived class对象的array使用。假设Base::operator new[]中每个元素对象大小是sizeof(Base),这样就是说你不能假设array元素个数是(bytes申请数/sizeof(Base))。传递给operator new[]的size_t参数,其值有可能比将辈填对象的内存大一些,因为条款 16提过,动态分配的arrays可能包含额外空间用来存放元素个数。

operator delete实现

void operator delete(void* rawMemory) throw()
{
    if(rawMemory==0) return;

    归还rawMemory所指内存;
}

member operator delete版本

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

如果即将删除的对象派生自某个base class而后者没有virtual析构函数,那么C++传给operator delete的size_t数值可能不正确。

请记住

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

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

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

Widget* pw = new Widget;

有两个函数被调用,一个是用以分配内存的operator new,一个是widget的default的构造函数,如果第二个函数抛出异常,此时,pw未被赋值,客户没有能力归还内存。此任务落到C++运行期系统上,运行期系统会调用operator new 对应的operator delete,这不是问题,对于正常的operator new:

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

它有对应的operator delete:

void operator delete(void* rawMemory)throw();//global作用域中的正常签名式
void operator delete(void* rawMemory)throw();//class 作用域典型的签名式

只要你使用正常形式的new和delete,运行期可以找出那个对应的delete,但是如果你声明了附带参数的operator new,究竟哪一个delete伴随这个new的问题变浮现了

举个例子,该class拥有一个 专属的operator new,接受一个ostream来志记相关分配信息(这个代码设计有问题)

class Widget{
public:
    ……
    static void* operator new(std::size_t size, std::ostream& logStream)//非正常形式的new
        throw(std::bad_alloc);
    static void operator delete(void* pMemory, std::size_t size)//正常的class专属delete
        throw();
    ……
};

先讨论若干术语

如果operator new的参数除了一个size_t之外还有其他参数,这便是所谓的placement new,上述的operator new是个placement版本,众多placement new版本特别有用的一个指针,其指向被构造对象处,那样的operator new 长相如下

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

这个版本的new被纳入C++标准程序库。当人们谈到placement new,大多数指的是这一特定版本,少数情况下指的是接受任意额外实参的operator new

现在回到有设计问题的那个widget类中。

考虑以下用户代码

Widget* pw = new(std::cerr)	Widget

如果内存分配成功,而Widget的构造函数抛出异常,运行期系统有责任取消operator new的分配并恢复旧观,正如我们刚开始所讲的那样,运行期系统需要找到那个与参数个数和类型与operator new相同的某个operator delete,所以对应的operator delete的就应该是

void operator delete(void*,std::ostream&)

类似于new的placement版本,opeartor delete 如果声明接受额外参数,便称为placement deletes,widget并没有声明placement版本的operator delete,所以当此错误发生,不会有任何的operator delete被调用。

所以widget的定义应该如下

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;//调用operator new,并传递cerr作为ostream实参

还有一点需要记住的是成员函数的名称会遮掩其外围作用域的相同名称(条款33)

class Base{
public:
    ……
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);//会掩盖global new
    ……
};
Base* pb=new Base;//错误,因为正常形式的operator new被掩盖
Base* pb1=new (std::cerr) Base;//调用Base的placement new

对于derived class的operator new会遮盖继承而来的operator new 和global版本的new

 class Derived: public Base{
    public:
        ……
        static void* operator new(std::size_t size) throw(std::bad_alloc);//重新声明正常形式的new
    };
    Derived* pd=new (std::clog) Derived;//错误,因为Base的placement new被掩盖了
    Derived* pd1=new Derived;//正确
在缺省情况下,C++在global作用域内提供以下形式的operator new:

    void* operator(std::size_t) throw(std::bad_alloc);//normal new
    void* operator(std::size_t, void*) throw();//placement new
    void* operator(std::size_t, const std::nothrow_t&) throw();//nothrow new

解决方法:

建立一个base class,内含所有正常形式的new和delete

class StadardNewDeleteForms{
public:
    //normal
    static void* operator new(std::size_t size) throw(std::bad_alloc)
    {return ::operator new(size);}
    static void operator delete(void* pMemory) throw()
    {::operator delete(pMemory);}
    //placement
    static void* operator new(std::size_t size, void* ptr) throw(std::bad_alloc)
    {return ::operator new(size, ptr);}
    static void operator delete(void* pMemory, void* ptr) throw()
    {::operator delete(pMemory, ptr);}
    //nothrow
    static void* operator new(std::size_t size, const std::nothrow_t& nt) throw(std::bad_alloc)
    {return ::operator new(size,nt);}
    static void operator delete(void* pMemory,const std::nothrow_t&) throw()
    {::operator delete(pMemory);}
};

如果想以自定义方式扩充标准形式,可以使用继承机制和using声明

 class Widget: public StandardNewDeleteForms{
public:
    //让这些形式可见
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;
    //添加自己定义的
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std:;bad_alloc);
    static void operator detele(std::size_t size, std::ostream& logStream) throw();
};

请记住

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

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

posted @ 2022-11-05 11:53  WetYu  阅读(3)  评论(0)    收藏  举报