26 限制某个class所能产生的对象数量

  • 允许零个或一个对象

每当即将产生一个对象,我们确知一件事情:会有一个constructor被调用。“阻止某个class产出对象”的最简单方法就是将其constructorsh声明为private:

class CantBeInstantiated
{
private:
	CantBeInstantiated();
	CantBeInstantiated(const CantBeInstantiated&);
	...
};

此举移除了每个人产出对象的权利,但是我们也可以选择性地解除这项约束。举个例子,假设我想为打印机设计一个class,我希望设下”只能存在一台打印机“的约束,我们可以将打印机对象封装在某个函数内,如此一来每个人都能够获取打印机,但只有唯一一个打印进对象被产生:

class PrintJob; //前置声明
class Printer
{
public:
	void submitJob(const PrintJob& job);
	void reset();
	void performSelfTest();
	...
	friend Printer& thePrinter();
	
private:
	Printer();
	Printer(const Printer& rhs);
	...
};

Printer& thePrinter()
{
	static Printer p; //唯一的一个打印机对象
	return p;
}

此设计有3个成分,第一,Printer class的constructors属性为private,可以压制对象的产生;第二,全局函数thePrinter被声明为此class的一个friend,致使thePrinter不受private constructors的约束;第三,thePrinter内含一个static Printer对象,意思只有一个Printer对象被产生出来。

一旦client需要和系统中唯一那台打印机打交道,就调用thePrinter。由于此函数返回一个reference,代表一个Printer对象,所以thePrinter可以用在任何需要Printer对象的地方:

class PrintJob
{
public:
	PrintJob(const string& whatToPrint);
	...
};

string buffer;
...				//把数据放进buffer
thePrinter.reset();
thePrinter.submitJob(buffer);

可能将thePrinter加入全局命名空间之中,可能会令你不悦。将thePrinter设置成Printer的一个static member function完成你的愿望,同时消除了friend的必要性。改用static member function之后,Printer看起来像这样:

class Printer
{
public:
	static Printer& thePrinter();
	...
private:
	Printer();
	Printer(const Printer& rhs);
	...
};

PrintJob& Printer::thePrinter()
{
	static Printer p;
	return p;
}

现在,client取用打印进时,会稍微冗长些:

Printer::thePrinter().reset();
Printer::thePrinter().submitJob(buffer);

另一个做法就是把Printer和thePrinter从全局空间移走,放进一个namespace内。namespace可以组织名字冲突。如下就是把Printer和thePrinter放进一个名为PrintingStuff的namespace中:

namespace PrintingStuff{
	class PrintJob; //前置声明
	class Printer
	{
	public:
		void submitJob(const PrintJob& job);
		void reset();
		void performSelfTest();
		...
		friend Printer& thePrinter();
		
	private:
		Printer();
		Printer(const Printer& rhs);
		...
	};

	Printer& thePrinter()
	{
		static Printer p; //唯一的一个打印机对象
		return p;
	}

}

有了这个namespace,clients可以使用完全限定名来获取thePrinter:

PrintingStuff::thePrinter().reset();
PrintingStuff::thePrinter().submitJob(buffer);

也可以利用using declaration来少打几个字:

using PrintingStuff::thePrinter;
thePrinter().reset();
thePrinter().submitJob(buffer);

在此thePrinter的实现代码中,有两个精细的地方值得讨论。第一,形成唯一的一个Printer对象,是函数中的static对象而非class中的static对象,这一点很重要。“class拥有一个static对象”的意思:即使从未被用到,它也会被构造(及析构)。相反的”函数拥有一个static对象“的意思:此对象在函数第一次被调用时才产生,如果该函数从未被调用,这个对象也就绝不会诞生(然而你必须付出代价,在函数每次被调用时检查对象是否需要诞生)。C++的一个哲学基础shi,你不应该为你不适用的东西付出任何代价,而”将打印机这类对象定义为函数内的一个static“,正是固守此哲学一种做法。

让打印机成为一个class static而非一个function static,另一个缺点,那就是它的初始化时机。我们确切知道一个function static的初始化时机:在函数第一次被调用,并且在该static被定义处。至于一个class static(或者global static,如果你不认为那样很拙劣的话)则不一定在什么时候初始化。C++对于“同一编译单元内的statics”初始化顺序是有提出一些保证的,但对于“不同编译单元内的statics”的初始化顺序没有任何说明。事实上,这成为无数头痛问题的来源。

第二个细微点事函数的“static对象与inline互动”。再次看看thePrinter的non-member版本:

	Printer& thePrinter()
	{
		static Printer p; //唯一的一个打印机对象
		return p;
	}

inline概念上意味着编译器应该将每一个调用以函数本身取代,但对于non-member functions,它意味着其他一些事情,它意味着这个函数有内部链接。

函数如果带有内部链接,可能会在程序中被复制,也就是说程序的目标代码可能会对带有内部链接的函数复制一份以上的代码,而此复制行为也包括函数的static对象。如果你有一个inline non-member function 并于其中内含一个local static对象,你的程序可能会拥有多份该static对象的副本。所以千万不要产生内含local static对象inline non-member functions(现在的编译器可以了,96年前的编译器不支持inline non-member functions)。

但或许你认为“产生一个函数,返回一个reference指向某隐藏对象“,是限制对象个数的一个错误路线;或许你认为比较好的方法是简单计算目前存在的对象个数,并于外界申请ta多对象时,在constructor内抛出一个exception。换句话说,你或许认为我们应该这样处理打印机的诞生:

class Printer
{
public:
	class TooManyObjects{};	//当外界申请太多对象时,抛出异常类
	Printer();
	~Printer();
	...
private:
	static size_t numObjects;
	Printer(const Printer& rhs);//打印机个数永远为1,所以不允许拷贝构造
};

现在的想法是,利用numObjects来追踪目前存在多少个Printer对象。这个数值将在constructor中累加,并在destructor中递减。如果外界企图构造太多的Printer对象,我们就抛出一个类型为TooManyObjects的exception:

size_t Printer::numObjects = 0;

Printer::Printer()
{
	if(numObjects >= 1)
		throw TooManyObjects();
	proceed with normal construction here;
	++numObjects;
}
Printer::~Printer()
{
	perform normal desturction here;
	--numObjects;
}

这种限制对象的诞生方法有许多吸引人的理由。至少它非常直接易懂——每个人都应该能够了解其行为。另一个理由是,它很容易被一般化,使对象的最大数量可以指定为1以外的值。

  • 不同的对象构造状态

但是这种策略也有问题。假设我们有一台特别的打印机,彩色打印机。其class和一般打印机有许多共同点,所以会让继承机制:

class ColorPrinter:public Printer
{
	...
};

现在假设我们的系统安装了一台一般打印机和一台彩色打印机:

Printer p;
ColorPrinter cp;

上述对象定义带给我们多少个Printer对象?答案是两个:一个是p,一个是cp的“Printer成分”。一旦执行,在cp的“base class成分”构造当时,会抛出TooManyObjects exception。“避免具体类(concrete class)继承其他具体类“这一设计准则可使你免受问题之苦。

当其他对象内含Printer对象时,类似问题再度发生:

class CPFMachine //针对那些可影印、可打印、可传真的机器
{
private:
	Printer p;		//针对打印
	FaxMachine f;	//针对传真
	CopyMachine c;	//针对影印
	...
};
CPFMachine m1;	//没问题
CPFMachine m2;	//排除一个TooManyObjects exception。

问题在于Printer对象可于3种不同状态下生存:1、它自己,2、派生类的“base class 成分”,3、内嵌于较大的对象之中。这些不同状态的呈现把”追踪目前存在的对象个数“的意义严重弄混了。

假设你需要class FSA,用来表现有限个数的“状态机器”(state machines,此等机器在许多情况下有用,其中一些导出用户界面的设计)。更进一步假设你希望允许任意数量的FSA对象产生,但你希望确保没有任何class继承自FSA。你可以设计FSA如下,同时满足上述两个条件:

class FSA
{
public:
	//pseudo(伪)constructor
	static FSA* makeFSA();
	static FSA* makeFSA(const FSA& rhs);
	...
private:
	FSA();
	FSA(const FSA& rhs);
	...
};

FSA* FSA::makeFSA()
{
	return new FSA();
}

FSA* FSA::makeFSA(const FSA& rhs)
{
	return new FSA(rhs);
}

不像thePrinter函数总是返回一个reference代表唯一对象,这里的每一个makeFSA pseudo-constructor都返回一个指针,指向独一无二的对象。即允许产生无限个数的FSA对象。

但是每一个pseudo-constructor都调用了new,意味着调用者记得要调用delete,也可以使用auto_ptr,这样自动调用了delete,防止内存泄漏:

//间接调用default FSA constructor
auto_ptr<FSA> pfsal(FSA::makeFSA());
//间接调用FSA copy constructor
auto_ptr<FSA> pfsa2()FSA::makeFSA(*pfsa1);
  • 允许对象生生灭灭

我们利用thePrinter函数将唯一对象的各种处理行为封装起来,此法虽然符合期望地限制了Printer对象的个数为1,却也限制我们在每次执行此程序只能有唯一一个Pritner对象,因此我们不能写出这样的代码:

create Printer object p1;
use p1;
destroy p1;

create Printer object p2;
use p2;
destroy p2;
...

上述动作并未在同一时间产生一个以上的Printer对象,但是它在程序的不同地点使用不同的Printer对象。如果这样不被允许的话,似乎不合理。毕竟我们并未违反“只有一台打印机的“的条件。

我们的办法是将稍早的对象计数(object-counting)和先前所见的(pseudo-constructor)伪构造函数结合起来:

class Printer
{
public:
	class TooManyObjects{};
	
	//pseudo-constructor
	static Printer* makePrinter();
	~Printer();
	void submitJob(const PrintJob& job);
	void reset();
	void performSelfTest();
	...

private:
	static size_t numObjects;
	Printer();
	Printer(const Printer& rhs);	//不要定义此函数,我们不允许复制
};

size_t Printer::numObjects = 0;

Printer::Printer()
{
	if(numObjects >= 1)
		throw TooManyObjects();
	proceed with normal object construction here;
	++numObjects;
}

Printer* Printer::makePrinter()
{
	return new Printer;
}

如果“一旦太多对象被申请,即抛出一个exception”的概念对你带来不当的困扰,你可以改为令pseudo-constructor返回一个null指针。当然了,clients因为必须在对此指针做任何动作之前检查其值。

Clients使用这个Printer class,就像使用任何其他class一样,只不过他们必须调用pseudo-constructor,取代真正的constructor:

Printer p;								//错误,default ctor是private
Printer* p2 = Printer::makePrinter();	//没问题,间接调用default ctor

Printer* p3 = *p2;						//错误,copy ctor是private

p2->performSelfTest();
p2->reset();
...
delete p2;

这项技术很容易被泛化为“任意个数(不止一个)的对象”。我们唯一要做的就是将原来写死的常量1改为class专属的一个数值,ra年后将复制对象的限制去掉。例如,下面修订后的Printer class,允许最多10个Printer对象同时存再:

class Printer
{
public:
	class TooManyObjects{};
	
	//pseudo-constructor
	static Printer* makePrinter();
	static Printer* makePrinter(const Printer& rhs);
	~Printer();
	void submitJob(const PrintJob& job);
	void reset();
	void performSelfTest();
	...

private:
	static size_t numObjects;
	static const size_t maxObjects = 10;
	
	Printer();
	Printer(const Printer& rhs);	//不要定义此函数,我们不允许复制
};

size_t Printer::numObjects = 0;
const size_t Printer::maxObjects;

Printer::Printer()
{
	if(numObjects >= maxObjects)
		throw TooManyObjects();
	...
}

Printer::Printer(const Printer& rhs)
{
	if(numObjects >= maxObjects)
		throw TooManyObjects();
	...
}

Printer* Printer::makePrinter()
{
	return new Printer;
}

Printer* Printer::makePrinter(const Printer& rhs)
{
	return new Printer(rhs);
}
  • 一个用来计算对象个数的Base Class

我们可以完成一个base class,作为对象的计数器,并让诸如Printer之类的classes继承它。Printer class内的计数器是static numObjects,所以我们必须将这个变量搬到一个用来计算对象个数的class内。然而,我们也必须确保计算对象的每一个class都各自有一个计数器。因此我们可以令计数器为“template所产生的classes”内的一个static member:

template<class BeingCounted>
class Counted
{
public:
	class TooManyObjects{};
	static int objectCount(){ return numObjects;}
protected:
	Counted();
	Counted(const Counted& rhs);
	~Counted(){ --numObjects;}
private:
	static int numObjects;
	static const size_t maxObjects;
	void init();			//用来避免ctor代码重复出现
};

template<class BeingCounted>
Counted<BeingCounted>::Counted()
{
	init();
}

template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
{
	init();
}

template<class BeingCounted>
Counted<BeingCounted>::init()
{
	if(numObjects >= maxObjects) 
		throw TooManyObjects();
	++numObjects;
}

此template所产生的classes,只被设计作为base classes,因此才有所谓的protected constructors和destructor存在。

现在我们修改Pirnter class,让它运用Counted template:

class Printer:private Counted<Printer>
{
public:
	//pseudo-constructors
	static Printer* makePrinter);
	static Printer* makePrinter(const Printer& rhs);
	~Printer();
	
	void submitJob(const PrintJob& job);
	void reset();
	void performSelfTest();
	...
	using Counted<Printer>::objectCount;
	using Counted<Printer>::TooManyObjects;
	
private:
	Printer();
	Printer(const Printer& rhs);
};

“Printer利用Counted template追踪目前存在有多少个Printer对象”这件事,除了Printer作者以外,和任何人无关。此等实现细节最好是private,这就是为什么此处使用private inheritance。

另一种做法是public inheritance,但是这样一来必须将Counted classes的析构函数设置成virtual destructor,虚函数出现会影响Counted所派生的对象的大小及内存布局,我们不希望有这些开销,所以最好用private inheritance。

Counted的大部分都隐藏起来不让Printer的用户知道,但是某些用户希望知道有多少个Printer对象存在,Counted template提供了一个objectCount函数,但是该函数私有继承后,访问权限变成了private访问层,可以通过using declaration来回复public:

class Printer:private Counted<Printer>
{
public:
	...
	using Counted<Printer>::objectCount; //让Printer用户访问objectCount
};

有一个不够结实的尾巴需要绑紧一点,那就是关于Counted内部的statics义务性定义。是的,我们很容易处理numObjects——只需要将一下两行放进Counted的某个实现文件即可:

template<class BeingCounted>
int Counted<BeingCounted>::numObjects; //定义,自动初始化为0

但maxObjects的情况棘手,我们应该将此变量初始化为什么值呢?如果希望10台打印机,就该将Counted<BeingCounted>::maxObjects初始化为10,如果希望最多16个,就该将Counted<BeingCounted>::maxObjects初始化为16。

我们采用无为而治的态度,什么都不做,我们要求class的用户提供适当的初始化行为。Printer的作者必须在某个实现文件中加入这行:

template<class BeingCounted>
size_t Counted<BeingCounted>::maxObjects = 10;

如果作者忘记这样的定义,连接器就会报错,因为maxObjects未定义。

猜你喜欢

转载自blog.csdn.net/weixin_28712713/article/details/81636717
26