一 要求对象产生在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内.