• MoreEffectiveC++Item35 条款27: 要求或禁止对象产生于heap中


    一 要求对象产生在heap中

    阻止对象产生产生在non-heap中最简单的方法是将其构造或析构函数声明在private下,用一个public的函数去调用起构造和析构函数

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

     那么调用的时候

    UPNumber n; //错误,虽然编译可以通过,但是当n的dtor稍后被隐式调用,就不合法了
    UPNumber *p = new UPNumber; //正确
    ...
    delete p; // 错误! 试图调用private 析构函数
    p->destroy();//正确

    虽然也可以构造函数声明成private,但是构造函数的类型比较多(拷贝构造,默认构造,我们必须把这些构造函数都声明成private,所以还是控制析构函数的权限比较好,因为析构函数只有一个)

    如果涉及到继承,那么上述的方法就不好用了,我们看下面代码

    class UPNumber { ... }; // 声明析构函数或构造函数为 private
    class NonNegativeUPNumber:
    public UPNumber { ... }; // 错误! 析构函数或构造函数不能编译
    class Asset {
    private:
    UPNumber value;
    ... // 错误! 析构函数或构造函数不能编译
    };

    为了克服上面的困难,我们可以将私有权限(private)改为保护权限(protected)

    class UPNumber { ... }; // 声明析构函数为 protected
    class NonNegativeUPNumber:
    public UPNumber { ... }; // 现在正确了; 派生类
    // 能够访问其 protected 成员
    class Asset {
    public:
    Asset(int initValue);
    ~Asset();
    ...
    private:
    UPNumber *value;
    };
    Asset::Asset(int initValue)
    : value(new UPNumber(initValue)) // 正确
    { ... }
    Asset::~Asset()
    { value->destroy(); } // 也正确

    二 判断对象是否位于heap中

    在上述方法中,虽然你可以控制当前类为non-heap,但是你无法控制其父类是否为non-heap

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

    那么我们可以控制其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,供下一个对象使用
    }

    上面的观点是.创建对象时,如果调用operator new(堆对象).然后在构造函数中检测,并将flag值设回false.想法是对的,但是不好用.

    问题1

    UPNumber *numberArray=new UPNumber[100];

    它调用的构造函数是opreator new[],这个时候你会想我们重载一下这个方法不就可以了吗.

    答案是不行!虽然会调用100次construction,但是只有第一个构造函数会分配内存,其余的99次都不会调用operator new.所以调用第二次构造函数时会抛出异常.

    问题2

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

    我们以为他的调用顺序如下(过程1)

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

    但是某些编译器的调用过程是(过程2)

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

    如果是按照过程2的调用过程的话调用到步骤3时将会是onHeap设成false,第四步将会抛出异常

    下面介绍一下栈和堆的地址图

    栈位于高地址,堆位于低地址,在分配内存的过程中栈向下生长,堆向上生长

    那么这个时候我们可以判断我们创建的对象是否在堆中(下面的方法有误)

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

    但是我们忽略了static

    void allocateSomeObjects()
    {
    char *pc = new char; // 堆对象: onHeap(pc) 将返回 true
    char c; // 栈对象: onHeap(&c) 将返回 false
    static char sc; // 静态对象: onHeap(&sc) 将返回 true
    ...
    }

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

    class Asset {
    private:
    UPNumber value;
    ...
    };
    Asset *pa = new Asset;

    很明显*pa(包括它的成员 value)在堆上。同样很明显在指向 pa->value 上调用 delete是不安全的,因为该指针不是被 new 返回的

    下面介绍一个判断delete是否安全的方法

    void *operator new(size_t size)
    {
    void *p = getMemory(size); //调用一些函数来分配内存,
    //处理内存不够的情况
    把 p 加入到一个被分配地址的集合;
    return p;
    }
    void operator delete(void *ptr)
    {
    releaseMemory(ptr); // return memory to
    // free store
    从被分配地址的集合中移去 ptr;
    }
    bool isSafeToDelete(const void *address)
    {
    返回 address 是否在被分配地址的集合中;
    }

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

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

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

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

    为了实现安全delete我们可以使用mixin模式,设计一abstract base class

    class HeapTracked { // 混合类; 跟踪
    public: // 从 operator new 返回的 ptr
    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;
    };

    这个类使用了 list(链表)数据结构跟踪从 operator new 返回的所有指针,list 是标准 C++库的一部分(参见 Effective C++条款 49 和本书条款 35)。operator new 函数分配内存并把地址加入到 list 中;operator delete 用来释放内存并从 list 中移去地址元素.isOnHeap 判断一个对象的地址是否在 list 中.

    它的实现如下

    // mandatory definition of static class member
    list<RawAddress> HeapTracked::addresses;
    // HeapTracked 的析构函数是纯虚函数,使得该类变为抽象类。
    // (参见 Effective C++条款 14). 然而析构函数必须被定义,
    //所以我们做了一个空定义。.
    HeapTracked::~HeapTracked() {}
    void * HeapTracked::operator new(size_t size)
    {
    void *memPtr = ::operator new(size); // 获得内存
    addresses.push_front(memPtr); // 把地址放到 list 的前端
    return memPtr;
    }
    void HeapTracked::operator delete(void *ptr)
    {
    //得到一个 "iterator",用来识别 list 元素包含的 ptr;
    //有关细节参见条款 35
    list<RawAddress>::iterator it =
    find(addresses.begin(), addresses.end(), ptr);
    if (it != addresses.end()) { // 如果发现一个元素
    addresses.erase(it); //则删除该元素
    ::operator delete(ptr); // 释放内存
    } else { // 否则
    throw MissingAddress(); // ptr 就不是用 operator new
    } // 分配的,所以抛出一个异常
    }
    bool HeapTracked::isOnHeap() const
    {
    // 得到一个指针,指向*this 占据的内存空间的起始处,
    // 有关细节参见下面的讨论
    const void *rawAddress = dynamic_cast<const void*>(this);
    // 在 operator new 返回的地址 list 中查到指针
    list<RawAddress>::iterator it =
    find(addresses.begin(), addresses.end(), rawAddress);
    return it != addresses.end(); // 返回 it 是否被找到
    }

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

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

        这里利用了dynamic_cast<void*>的一个特性——它返回的指针指向原生指针的内存起始处,从而解决了策略3的多继承对象内存不唯一问题.(要使用dynamic_cast,要求对象至少有一个virtual function).任何类如果需要判断delete是否安全,只需要继承HeapTracked即可.

    调用时:

    class Asset: public HeapTracked {
    private:
    UPNumber value;
    ...
    };
    //我们能够这样查询 Assert*指针,如下所示:
    void inventoryAsset(const Asset *ap)
    {
    if (ap->isOnHeap()) {
    ap is a heap-based asset — inventory it as such;
    }
    else {
    ap is a non-heap-based asset — record it that way;
    }
    }

    三 禁止对象产生在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);
    ...
    };

    调用如下

    UPNumber n1; // okay
    static UPNumber n2; // also okay
    UPNumber *p = new UPNumber; // error! attempt to call
    // private operator new

    继承时:

    class UPNumber { ... }; // 同上
    class NonNegativeUPNumber: //假设这个类
    public UPNumber { //没有重写 operator new
    ...
    };
    NonNegativeUPNumber n1; // 正确
    static NonNegativeUPNumber n2; // 也正确
    NonNegativeUPNumber *p = // 错误! 试图调用
    new NonNegativeUPNumber; // private operator new

    内含时:

    class Asset {
    public:
    Asset(int initValue);
    ...
    private:
    UPNumber value;
    };
    Asset *pa = new Asset(100); // 正确, 调用 Asset::operator new 不是 UPNumber::operator new

    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内.

    总结,没有一个有效办法判断一个对象是否位于heap内.

  • 相关阅读:
    linq to access 简单实现 实例demo
    FCKEDITOR中文使用说明 js调用
    asp.net mvc 随想
    fccms 小型简单个人blog源码
    PHP文件上传路径
    前端优化技巧(一)
    会话框拖拽效果实现
    phpmailer配置
    上传图片动态预览(兼容主流浏览器)
    Java将多个list对象根据属性分组后合并成一个新的集合
  • 原文地址:https://www.cnblogs.com/LearningTheLoad/p/6995821.html
Copyright © 2020-2023  润新知