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

 

posted @ 2017-06-13 00:10  WangZijian  阅读(276)  评论(0编辑  收藏  举报