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内。我们做不到前者,当然我们也做不到后者。

猜你喜欢

转载自blog.csdn.net/weixin_28712713/article/details/81865096
27