27 要求(或禁止)对象产生于heap之中
有时候,我们要求该类型的对象被分配与heap内,能够“delete this”;另一些时候,我们要求拥有某种确定性,保证某一些类型绝不会发生内存泄漏,原因是没有任何一个该类型的对象从heap中分配出来。
- 要求对象产生于heap之中(所谓的Heap-Based Objects)
为了限制对象产生于heap,我们必须找到一个方法,阻止clients不得使用new以外的方法产生对象。non-heap objects会在其定义点自动构造,并在其寿命结束时自动析构,所以只有让那些被隐式调用的构造函数和析构函数不合法,就可以了。
欲让这些动作调用不合法,最直截了当的方式将constructors和destructor声明为private。但是这样太过了,没有理由让它们都成为private。比较好的方法是让destructor成为private,而constructors仍为public。如此一来,你可以导入一个pseudo destructor函数,用来调用真正的destructor。Clients则调用pseudo-destructor来销毁它们产生的对象。
例如,假设我们希望确保“表现无限精度”的数值对象只能诞生于heap之中。可以这么做:
class UPNumber
{
public:
UPNumber();
UPNumber(int initValue);
UPNumber(double initValue);
UPNumber(const UPNumber& rhs);
private:
~UPNumber();//dtor位于private
};
Clients于是应该这么写:
UPNumber n; //错误(虽然合法,但当dtor被调用就不合法了)
UPNumber* p = new UPNumber;
...
delete p; //错误,企图调用private destructor
p->destroy();
另一个办法,将所有的constructor都声明为private。这个办法的缺点就是,class常常有许多的constructors,其中包括copy constructor,也可能报货default constructor;class作者必须记住将它们每一个都声明为private。如果这些函数由编译器产生,编译器产生的函数总为public。所以比较容易的办法还是只声明destructor为private,因为一个class只有一个destructor。
只要限制destructor和constructors的运用,便可阻止non-heap objects的诞生,但是,它妨碍了继承和内含:
class UPNumber; //将dtor或ctor声明为private
class NonNegativeUPNumber:public UPNumber//错误,dtor或ctors无法通过编译
{...};
clas Asset
{
private:
UPNumber value; //错误,dtor或ctors无法通过编译
};
这些困难可以克服。令UPNumber的destructor成为protected(并仍保持其constructors为public),便可以解决继承问题。至于“必须内含UPNumber对象”之classes,可以修改为“内含一个指针,指向UPNumer”对象。
class UPNumber; //将dtor或ctor声明为protected
class NonNegativeUPNumber:public UPNumber//derived classes可以调用
{...}; //protected members
clas Asset
{
private:
UPNumber* value;
};
Asset::Asset(int value):
value(new UPNumber(initValue))
{
...
}
Asset::~Asset()
{
value->destroy();
}
- 判断某对象是否位于Heap内
我们如何约束对象都必须位于heap中,没有简单的方法。UPNumber constructor不可能知道它之所以调用是否是为了产生某个heap-based object的“base class成分”。也就是说没有办法可以让UPNumber constructor侦测一下状态有什么不同:
NonNegativeUPNumber* n1 = new NonNegativeUPNumber; //在heap
NonNegativeUPNumber n2; //不在heap
或许你认为可以在new operator、operator new以及“new operator所调用的constructor”三方互动关系上玩把戏。或许你认为只要把UPNumber修改成这样就行:
class UPNumber
{
public:
class HeapConstrainViolation{};
static void* operator new(size_t size);
UPNumber();
...
private:
static bool onTheHeap; //这一flag用来在ctor内指示正构造
}; //中的对象是否位于heap
boo UPNumber::onTheHeap = false;
void* UPNumber::operator new(size_t size)
{
onTheHeap = true;
return ::operator new(size);
}
UPNumber::UPNumber()
{
if(!onTheHeap)
throw HeapConstrainViolation();
proceed with normal construction here;
onTheHeap = flase; //清楚flag,供下一个对象使用
}
但是上述代码不能够用于new出来的UPNumber数组。数组由operator new[]而非operator new分配。你依然可以自己撰写*版,做相同的手脚。但是如果数组有100个元素,应该有100次constructor调用,但是整个程序只有一次分配动作,所以100次constructor只有第一次OntheHeap为true,第二次就会抛异常。
即使没数组,考虑如下代码:
UPNumber* pn = new UPNumber(*new UPNumber);
此时heap内产生两个UPNumbers,并让pn指向了其中一个。换句话说某对象以另一对象作为初值。这段代码会造成内存泄漏,先忽略,先看看如下动作执行为发生什么事:
new UPNumber(*new UPNumber);
这里内含两次new operator调用,并造成两次operator new和两次UPNumber constructors调用动作。程序员期望这些函数调用的执行顺序如下:
1.为第一个对象调用operator new。
2.为第一个对象调用constructor。
3.为第二个对象调用operator new。
4.为第二个对象调用constructor。
但是C++并不保证这样,某些编译器可能产生如下的顺序:
1.为第一个对象调用operator new。
2.为第二个对象调用operator new。
3.为第一个对象调用constructor。
4.为第二个对象调用constructor。
产生这样的代码,对于编译器来说并非错误。但是上述“在operator new中设立位值”的伎俩因此失败。因为步骤1和步骤2中所设立的位在步骤3中被清除掉,造成步骤4所构造的对象认为它不处于heap上,显然它确实在heap之中。
-这些困难并不至于造成“令每一个constructor检查*this是否位于heap内”的基本观念无效。在operator new(或operator new[])内检查某个位是否被置位,并不能决定“*this是否位于heap内”的可靠做法。
如果你对此绝望,可能会被诱入“不具移植性”的领域中。例如,你可能决定利用许多系统都有的一个事实:程序的地址空间以线性序列组织而成,其中stack(栈)高地址往低地址成长,heap(堆)由低地址往高地址成长:
有的系统是这样,有的系统不是这样。如果你的系统确如此方式来组织应程序的内存,你获悉可以由以下函数决定某个地址是否位于heap内:
//不正确的企图,决定某个位置是否位于heap之类
bool onHeap(const void* address)
{
char onTheStack; //local stack variable
return address < &onTheStack;
}
这个函数背后的观念很有趣。在OnHeap函数内,OnTheStack是个局部变量,所以它被置于stack内,当OnHeap被调用,其stack frame会被放置在stack顶端,由于此架构中的stack向低地址成长,所以OnTheStack的地址一定比其他位于stack中的变量更低。因此,如果参数address比OnTheStack的地址还低的话,它就一定位于heap。
这个逻辑是正确的,但是不够完善。被声明为static对象,也包括global scope和namespace scope内的对象在程序执行期只初始化一次。如此对象必须安置于某处,这些对象既不是stack也不是heap。
它们一般视情况而定,但在“stack和heap安排的如上所示”的许多系统中,它们被置于heap之下。如果包含static对象,看起来像这样:
突然间OnHeap运行失败,它无法区分heap对象和static对象:
void allocateSomeObjects()
{
char* pc = new char; //heap object,onHeap返回true
char c; //stack object,onHeap返回false
static char sc; //static object,onHeap返回true
...
}
令人悲苦的是,不只没有一个“绝对具移植性“的办法可以决定某个对象是否位于heap内,甚至没有一个”颇具移植性“做法可以在大部分时候使用。这样你就一定走入不可移植的,因系统而已的阴暗角落。你不想这样就最好重新设计软件,避免需要判断某个对象是否位于heap内。
如果你想知道“对象是否位于heap”内,可能因为你想要为它调用delete是否安全,然而调用此动作是否安全和“指针是否指向一个位于heap内的对象”是两码事。因为,并非所有指向heap内的指针都可以被安全删除。再次考虑一个Asset对象,内含一个UPNumber对象:
classs Asset
{
private:
UPNumber value;
...
};
Asset* pa = new Asset;
显然*pa(及其成员)位于heap内,同样,对着一个指向pa->value的指针进行删除动作是不安全的。因为没有一个这样的指针是以new获得的。
但是,判断“指针的删除动作”是否安全,比判断“指针是否指向heap内的对象”简单一些。因为前者的判断依据只是:此指针是否由new返回。因此我们可以自行写一个operator new,所以这问题比较容易解决。下面是解决方法:
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 address;
}
bool isSafeToDelete(const void* address)
{
return whether address is in collection of
allocated addresses;
}
很简单,operator new负责把一些条目加入到“由动态分配而得的地址”所形成的集合中,operator delete负责把这些条目移除;isSafeToDelete负责查找该集合,看看某个地址是否在其中。如果这些operator new和operator delete函数都在全局范围内,这应该对所有类型(甚至是内建模型)都管用。
实际上,有3件事情消除我们对此设计的*。第一,不愿在全局空间内定义任何东西。我们知道全局空间只有一个,该空间自带operator new和operator delete,这样系统自带的被我们屏蔽,便是我们声明的软件不兼容于其他使用系统自带operator new和operator delete。第二,效率问题,如果没有必要,如果没有必要,何必为所有“heap”应用承担沉重的簿记工作呢?第三,似乎不可能实现出一个总是有效的作用的isSafeToDelete函数,困难在于,当对象设计多重继承或虚拟继承的基类时,会拥有多个地址,所以不能保证“交给isSafeToDelete”和“被operator new返回”的地址是同一个——纵使论及的对象的确分配于heap内。
我们想要的,是这些函数提供机能,但不附带全局命名空间的污染问题、额外的义务性负荷,以及正确性的疑虑。C++可以才艺abstract mixin base class(抽象混合式基类),abstract base class不能被实例化的base class,mixin class则提供个一组定义完好的能力,能够与其derived class所提供的其他任何能力兼容:
class HeapTracked //mixin class 追踪并记录被operator new返回的指针
{
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;
};
list记录所有由operator new返回的指针,operator new负责分配内存并将tiaomu(entries)加入list内;operator delete 负责释放内存从list身上移除条目;isOnHeap决定对象的地址是否在list内。
HeapTracked class实现很简单,因为真正的内存分配动作和释放动作交给全局的operator new和operator delete函数完成,而list class所拥有的函数又可以顺利完成安插、移除、查询行为。下面是HeapTracked的完整实现内容:
list<RawAddress> HeapTracked::addresses;
HeapTracked::~HeapTracked{}
void* HeapTracked::operator new(size_t size)
{
void* memPtr = ::operator new(size);
addresses.push_back(memPtr);
return memPtr;
}
void HeapTracked::operator delete(void* ptr)
{
list<RawAddress>::iterator it =
find(addresses.begin(),addresses.end(),ptr);
if(it != addresses.end())
{
address.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(address.begin(),address.end(),rawAddress);
return it != address.end();
}
对于如下的语句困惑的语句:
const void* rawAddress = dynamic_cast<const void*>(this);
凡涉及“多重虚拟基类“的对象,会拥有多个地址,并因此使用全局函数isSafeToDelete的撰写变得更加复杂。此问题也在isOnHeap中令我们苦恼,由于isOnHeap只实行与HeapTracked对象身上,我们可以利用dynamic_cast操作来的特殊性来消除这个问题,只要简单的将指针“动作转换”为void*(或const void*或volatile void*或const volatile void*),不过,dynamic_cast只适用于“所指对象至少有一个虚函数”的指针身上。所以将this动态转型成const void*,会为我们带来一个指针,指向现行对象的内存起始处。那便是Heap::operator new必须返回的指针。
有了这样的class,可以为任何class加上“追踪指针”(指向heap分配所得)的能力。唯一要做的就是令class继承HeapTracked。举例,如果我们希望能够判断Asset对象指针是否指向一个heap-based object,可以修改Asset class的定义,令其继承HeapTracked:
class Asset: public HeapTracked
{
private:
UPNumber value;
...
}
然后我们便可以查询Asset*指针如下:
void inventoryAsset(const Asset* ap)
{
if(ap->isOnHeap())
ap is a head-based asset -inventory it as such;
else
ap is a non-heap-based asset - record it that way;
}
这样的HeapTracked这样的mixin class有个缺点,不能用于内置类型身上。因为int和char这类类型并不继承任何东西。
- 禁止对象产生于heap之中
一般而言有三种可能:(1)对象被直接实例化;(2)对象被实例化为derived class objects内的”base class“成分;(3)对象被内嵌于其他对象之中。
欲阻止clients直接将对象实例化于heap之中,很容易,因为此等对象总是以new产生出来,你可以让client无法调用new,虽然你不能影响new operator的能力,但是你可以自行定义operator new,将其声明为private。举例子,如果你不希望clients将UPNumber对象产生于heap内,你可以这么做:
class UPNumber
{
private:
static void* operator new(size_t size);
static void operator delete(void* ptr);
...
};
现在clients只能做一些被允许的事情:
UPNumber n1; //可以
static UPNumber n2; //可以
UPNumber* p = new UPNumberl //错误,企图调用private函数
如果要禁止产生“UPNumer对象所组成的数组”,可以将operator new[]和operator delete[]声明为private。
有趣的是将operator new声明为private,往往也会妨碍UNumber对象被实例化为heap-based derived class objects的“base class成分”。那是因为operator new和operator delete会被继承,所以如果这些函数不在derived class声明为public,derived class继承的便是base(s)所声明的private版本:
class UPNumber{...};
class NonNegativeUPNumber:
public UPNumber
{
...
};
NonNegativeUPNumber n1; //可以
static NonNegativeUPNumber n2; //可以
NonNegativeUPNumber* p = new NonNegativeUPNumber; //错误
如果derived class声明一个属于自己的operator new(且为public),这样就能成功。类似情况,当我们企图分配一个“内含UPNumber对象”的对象,“UNPumber的operator new乃为private“不会带来什么影响:
class Asset
{
public:
Asset(int initValue);
...
private:
UPNumber value;
};
Asset* pa = new Asset(100); //没问题,调用的是
//Asset::operator new或
//::operator new而非UPNumber::operator new
我们曾经希望“如果一个UPNumber对象被构造于heap以外,那么就在UPNumber constructor内抛出异常”,这次我们希望“如果对象被产生于heap内的话,就抛出一个异常”。然而,就像没有任何根据移植性的做法可以判断某地址是否位于heap内一样,我们也没有根据移植性的做法可以判断它是否不再heap内。我们做不到前者,当然我们也做不到后者。