• More Effective C++ 条款27 要求(禁止)对象产生与heap之中


    1. 要求对象产生于堆中

        由于non-heap 对象会在定义时自动构造,并在寿命结束时自动析构,因此要阻止客户产生non-heap对象,只需要将构造或析构函数声明为private.又由于构造函数可能有多个,儿媳够函数只有一个,因此更好的选择是将析构函数声明为private,然后开放一接口调用它,像这样:

    class UPNumber {
    public:
        UPNumber();
        UPNumber(int initValue);
        UPNumber(double initValue);
        UPNumber(const UPNumber& rhs);
        // pseudo destructor,它是const menber function,因为const对象也需要被销毁
        void destroy() const { delete this; }
        ...
    private:
        ~UPNumber();
    };
    View Code

        对UPNumber的使用像这样:

    UPNumber n; //错误
    UPNumber *p = new UPNumber; //正确
    ...
    delete p; // 错误! 试图调用private 析构函数
    p->destroy();//正确
    View Code

        通过限制costructor或destructor的使用便可以阻止non-heap对象的产生,但同时也阻止了继承和内含:

    class UPNumber { ... }; //将析构函数或构造函数声明为private
    //继承
    class NonNegativeUPNumber: public UPNumber { ... }; // 错误! 不能通过编译
    //内含
    class Asset {
    private:
        UPNumber value;
        ... // 错误! 不能通过编译
    };
    View Code

        对于继承,解决方法就是将UPNumber的构造函数或析构函数声明为protected,对于内含,解决方法则是将内含UPNmuber对象改为内含一个珍惜爱ingUPNumber对象的指针,像这样:

    class UPNumber { ... }; // 声明析构函数为protected
    class UPNumber { ... }; // 声明构造或析构函数为protected
    class NonNegativeUPNumber:
    public UPNumber { ... }; // 现在可以通过编译
    class Asset {
    public:
        Asset(int initValue);
        ~Asset();
        ...
    private:
        UPNumber *value;
    };
    Asset::Asset(int initValue)
    : value(new UPNumber(initValue))
    { ... }
    Asset::~Asset()
    { value->destroy(); }  
    View Code

    2. 判断某个对象是否位于heap内

        1中提出的方法依然不能解决在继承情况下基类可能位于non-heap的问题,用户可以产生一个non-heap NonNegativeUPNumber对象,但UPNumber却无法阻止,实际上它甚至无法知道自己是否是作为某个heap-based class的base class部分而产生,也就没有办法检测以下状态有什么不同:

    NonNegativeUPNumber *n1 = new NonNegativeUPNumber; // 在heap内
    NonNegativeUPNumber n2;//不在heap内

        策略1:利用"new 操作符调用operator new且我们可以对operator new进行重载"的特点(稍后会解释这种方法有硬伤),像这样:

    class UPNumber {
    public:
    // 如果产生一个非堆对象,就抛出异常
        class HeapConstraintViolation {};
        static void * operator new(size_t size);
        UPNumber();
        ...
    private:
        static bool onTheHeap; //标志对象是否被构造于堆上
        ... 
    };
    // 类外部定义静态成员
    bool UPNumber::onTheHeap = false;
    void *UPNumber::operator new(size_t size)
    {
        onTheHeap = true;
        return ::operator new(size);
    }
    UPNumber::UPNumber()
    {
        if (!onTheHeap) {
            throw HeapConstraintViolation();
        }
        proceed with normal construction here;
        onTheHeap = false;//清除flag
    }
    View Code

        这种方法对于产生单个对象的确可行:用户如果通过new来产生对象,onTheHeap就会在operator new中被设为true,构造函数被正常调用,如果对象不是产生于堆中,onTheHeap就为false,调用析构函数时就会抛出异常.但对于数组的产生却存在硬伤,对于以下代码:

    UPNumber *numberArray=new UPNumber[100];

        即使对operator new[]也进行了重载,使它具有和之前operator new类似的动作,但是由于调用new[]时,内存一次性分配而构造函数多次调用,因此operator new[]只能将onTheHeap在第一次调用构造函数时设为true,以后将不再调用operator new[],onTheHeap也只能为false,也就是说,为数组中的元素第二次调用构造函数时就会抛出异常.

        解决方法或许可以为UPNumber再增加一个static bool型成员,用于标记对象是否是作为数组的元素而产生,但这样更加复杂而且容易出错,因此不再讨论.

        此外,即使没有数组,这种设计也可能会失败,对于以下操作:

    UPNumber *pn = new UPNumber(*new UPNumber);//会造成资源泄露,但是先不考虑这个问题

        通常认为operator new和构造函数的调用顺序如下:

    1.为第一个对象调用operator new
    2.为第一个对象调用constructor
    3.为第二个对象调用operator new
    4.为第二个对象调用constructor
    View Code

        "但C++并不保证这么做,某些编译器产生出来的函数调用顺序是这个样子:"

    1.为第一个对象调用operator new
    2.为第二个对象调用operator new
    3.为第一个对象调用constructor
    4.为第二个对象调用constructor
    View Code

        在此情况下策略1仍会失败.

        策略2:"利用许多系统都有的一个事实:程序的地址空间以现行序列组织而成,其中stack(栈)高地址往低地址成长,heap(堆)往低地址成长",像这样:

    bool onHeap(const void *address){
        char onTheStack; // 局部栈变量
        return address < &onTheStack;
    }
    View Code

        这种方法无疑不具有移植性,因为有的系统是这样,有的系统却不是这样,此外,static对象(包括global scope和namespace scope)既不是位于stack也不是位于heap中,它的位置视系统而定,可能位于heap之下,在这种情况下,策略2无法区分heap对象和static对象!

        从策略1和策略2可以看出,其实没有一个通用且有效的办法可以区分heap和stack对象,但是区分heap和stack的目的通常是为了判断对一个指针使用delete是否安全,幸运的是,实现后者比实现前者更容易,因为对象是否位于heap内和指针是否可以被delete并不完全等价,对于以下代码:

    struct Asset{
        int a;
        UPNumber value;
        ...
    }
    Asset* a(new Asset);
    UPNumber* ptr=&(a->value);
    View Code

        尽管ptr指向的是heap内存,但对ptr实行delete会出错,原因在于a是通过new取得,但它的成员——value并不是通过new取得.从这里可以看出,对一个指针使用delete是否安全并不取决于对象是否位于heap中,而是取决于它是否是通过new获得(其实本质上取决于它是否位于申请的一段heap内存的开始处,但这里不讨论).

        策略3(用于判断对指针delete是否安全):

    void *operator new(size_t size)
    {
        void *p = getMemory(size); //调用函数分配内存并处理内存不足的情况
        add p to the collection of allocated addresses;
        return p;
    }
    void operator delete(void *ptr)
    {
        releaseMemory(ptr); // 归还内存
        //remove ptr from the collection of allocated addresses
    }
    bool isSafeToDelete(const void *address)
    {
        return whether address is in collection of allocated addresses;
    }
    View Code

        这里采用了较朴素的方法,将由动态分配而来的地址加入到一个表中,isSafeToDelete负责查找特定地址是否在表中,从而判断delete是否安全.但策略3仍存在三个缺点:

        1). 需要重载全局版本的operator new和operator delete,这是应该尽量避免的,因为这会使程序不兼容于其他"也有全局版之operator new和operator delete"的任何软件(例如许多面向对象数据库系统).

        2). 需要维护一个表来承担簿记工作,这会消耗资源.

        3). 很难设计出一个总是能返回作用的isSafeToDelete函数,因为当对象涉及多重继承或虚继承的基类时,会拥有多个地址,因此不能保证"交给isSafeToDelete"和"被operator new返回"的地址是同一个,纵使使用delete是安全的,像这样:

    class Base1{
    public:
        virtual ~Base(){}
        ...
    private:
        int a;
    }
    class Base2{
    public:
        virtual ~Base2(){}
        ...
    private:
        int b;
    }
    class Derived:public Base1,public Base2{}
    Base2* ptr=new Derived;
    View Code

        ptr所指地址显然不在所维护的表中,因此isSafeToDelete返回false,但对ptr使用delete却是安全的,因为Base2的析构函数为虚.

        策略4:使用mixin模式,设计一abstract base class,用于提供一组定义完好的能力,像这样:

    class HeapTracked { 
    public: 
        class MissingAddress{}; // 异常类
        virtual ~HeapTracked() = 0;
        static void *operator new(size_t size);
        static void operator delete(void *ptr);
        bool isOnHeap() const;
    private:
        typedef const void* RawAddress;
        static list<RawAddress> addresses;//维护heap地址的表
    }list<RawAddress> HeapTracked::addresses;
    
    // 析构函数设为纯虚函数以使得该类成为抽象类,但必须有定义.
    HeapTracked::~HeapTracked() {}
    void * HeapTracked::operator new(size_t size)
    {
        void *memPtr = ::operator new(size); 
        addresses.push_front(memPtr); // 在表中插入新地址
        return memPtr;
    }
    void HeapTracked::operator delete(void *ptr)
    {
        //查找是否在表中
        list<RawAddress>::iterator it =find(addresses.begin(), addresses.end(), ptr);
        if (it != addresses.end()) { 
            addresses.erase(it); 
            ::operator delete(ptr); 
        } 
        else {
            throw MissingAddress(); 
        } 
    }
    bool HeapTracked::isOnHeap() const
    {
        // 得到一个指针,指向*this占据的内存空间的起始处,
        const void *rawAddress = dynamic_cast<const void*>(this);
        // 在表中查找
        list<RawAddress>::iterator it =find(addresses.begin(), addresses.end(), rawAddress);
        return it != addresses.end(); // 返回it是否被找到
    }
    View Code

        唯一需要解释的一点就是isOnTheHeap中的以下语句:

    const void *rawAddress = dynamic_cast<const void*>(this);

        这里利用了dynamic_cast<void*>的一个特性——它返回的指针指向原生指针的内存起始处,从而解决了策略3的多继承对象内存不唯一问题.(要使用dynamic_cast,要求对象至少有一个virtual function).

        任何类如果需要判断delete是否安全,只需要继承HeapTracked即可.

    3. 禁止对象产生于heap之中

        对象的存在形式有三种可能(之前已提到过):1) 对象被直接实例化 2)对象被实例化为derived class objects内的"base class 成分" 3)对象被内嵌与其他对象之中     

        要阻止对象直接实例化与heap之中,只要利用"new 操作符调用opearator new而我们可以重载operator new"的原理即可,将operator new或operator delete设为private,像这样:

    class UPNumber {
    private:
        static void *operator new(size_t size);
        static void operator delete(void *ptr);
        ...
    };
    View Code

        operator new和operator delete一同设为private是为了统一它们的访问层级,值得注意的是,将operator new声明为private,也会阻止UPNumber对象被实例化为heap-based derived class objects的"base class 成分",因为operator new和operator delete都会被继承,如果这些函数不再derived class中重定义,derived class使用的就是base class版本(但已被设为private)

        但如果derived class声明自己的operator new和operator delete或涉及到内含的情况时,对象仍然可能位于heap内,正如2所总结,没有一个有效办法判断一个对象是否位于heap内.

  • 相关阅读:
    mongodb安装
    node版本的管理 n
    npm 命令
    nodejs,npm安装(ubuntu14.04下)
    yeoman,grunt,bower安装(ubuntu14.04)
    什么是堆和栈,它们在哪儿?
    malloc函数详解 (与new对比)
    单链表的C++实现(采用模板类)
    短信验证码
    webapi
  • 原文地址:https://www.cnblogs.com/reasno/p/4856953.html
Copyright © 2020-2023  润新知