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

posted @ 2015-10-06 12:23  Reasno  阅读(405)  评论(0编辑  收藏  举报