1. 要求对象产生于堆中(只能用new)
由于non-heap 对象会在定义时自动构造,并在寿命结束时自动析构,因此要阻止客户产生non-heap对象(只产生heap对象),只需要将构造或析构函数声明为private.又由于构造函数可能有多个,而析构函数只有一个,因此更好的选择是将析构函数声明为private,然后开放一接口调用它,像这样:
class UPNumber {
public:
UPNumber();
UPNumber(int initValue);
UPNumber(double initValue);
UPNumber(const UPNumber& rhs);//copy constructor
// pseudo(伪的) destructor,它是const member function,因为const对象也需要被销毁(const成员函数存在的意义在于它能被const对象调用)
void destroy() const { delete this; }
...
private:
~UPNumber();
};
对UPNumber的使用像这样:
UPNumber n; //错误(虽然合法,但n的析构函数稍后被隐式调用,就不合法了)
UPNumber *p = new UPNumber; //正确
...
delete p; // 错误! 试图调用private 析构函数
p->destroy();//正确
通过限制costructor或destructor的使用便可以阻止non-heap对象的产生,但同时也阻止了继承和内含:
class UPNumber { ... }; //将析构函数或构造函数声明为private
//继承
class NonNegativeUPNumber: public UPNumber { ... }; // 错误! destructor或constructor不能通过编译
//内含
class Asset {
private:
UPNumber value;
... // 错误! destructor或constructor不能通过编译
};
对于继承,解决方法就是将UPNumber的构造函数或析构函数声明为protected。对于内含,解决方法则是将内含UPNmuber对象改为内含一个指向UPNumber对象的指针,像这样:
class UPNumber { ... }; // 声明析构函数为protected
class NonNegativeUPNumber:public UPNumber { ... }; // 现在可以通过编译,derived class可以取用protected member
class Asset {
public:
Asset(int initValue);
~Asset();
...
private:
UPNumber *value;//指向基类的指针
};
Asset::Asset(int initValue):value(new UPNumber(initValue)){ ... }
Asset::~Asset(){ value->destroy(); }
2. 判断某个对象是否位于heap内
1中提出的方法依然不能解决在继承情况下基类可能位于non-heap的问题,用户可以产生一个non-heap NonNegativeUPNumber对象:
NonNegativeUPNumber n;//没问题
但UPNumber却无法阻止,实际上它甚至无法知道自己是否是作为某个heap-based object的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,供下一个对象使用
}
这种方法对于产生单个对象的确可行:用户如果通过new来产生对象,onTheHeap就会在operator new中被设为true,构造函数被正常调用,如果对象不是产生于堆中,onTheHeap就为false,调用构造函数时就会抛
出类型为HeapConstraintViolation的异常.但对于数组的产生却存在硬伤,对于以下代码:
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);//会造成资源泄露,但是先不考虑这个问题。
此处于heap内产生了两个UPNumber,并让pn指向其中一个,也就是某对象以另一对象的值作为初值。会造成资源泄露
通常认为operator new和构造函数的调用顺序如下:
1.为第一个对象调用operator new 2.为第一个对象调用constructor 3.为第二个对象调用operator new 4.为第二个对象调用constructor
"但C++并不保证这么做,某些编译器产生出来的函数调用顺序是这个样子:"
1.为第一个对象调用operator new 2.为第二个对象调用operator new 3.为第一个对象调用constructor 4.为第二个对象调用constructor
在此情况下策略1仍会失败.(步骤1和2所设立的bool onTheHeap的值在步骤3被清除了,即设为了false,在步骤4所构造的对象会认为它不在heap中而抛出异常,但其实它在heap中)
策略2:"利用许多系统都有的一个事实:程序的地址空间以现行序列组织而成,其中stack(栈)高地址往低地址成长,heap(堆)往低地址成长",像这样:
bool onHeap(const void *address){//函数参数address
char onTheStack; // 局部栈变量
return address < &onTheStack;
}
(在onHeap函数内,onTheStack为局部变量,被置于stack中。当它被调用,onTheStack的地址比其他任何一个位于stack中的变量(或对象)更低,故若参数address比onTheStack的地址更低,它不可能位于栈中)
这种方法无疑不具有移植性,因为有的系统是这样,有的系统却不是这样,此外,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);
尽管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;//将p加入已分配地址的集合中
return p;
}
void operator delete(void *ptr)
{
releaseMemory(ptr); // 归还内存
//remove ptr from the collection of allocated addresses//从分配的地址集合中删除ptr
}
bool isSafeToDelete(const void *address)
{
return whether address is in collection of allocated addresses;//地址是否在已分配地址的集合中
}
这里采用了较朴素的方法,将由动态分配而来的地址加入到一个表中,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;
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是否被找到
}
唯一需要解释的一点就是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);
...
};
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内.