众所周知:只能生成一个对象的是单例模式,但是看一下情况:
假设定义一个Printer 类:
建立 Printer 对象 p1;
使用 p1;
释放 p1;
建立Printer对象p2;
使用 p2;
释放 p2;
...
这种设计在同一时间里没有实例化多个Printer对象, 而是在程序的不同部分使用了不
同的Printer对象。不允许这样编写有些不合理。毕竟我们没有违反只能存在一个printer
的约束。就没有办法使它合法化么?
当然有:
class Printer {
public:
class TooManyObjects{}; //异常类
// 伪构造函数
static Printer * makePrinter();
~Printer();
void submitJob(const PrintJob& job);
void reset();
void performSelfTest();
...
private:
static size_t numObjects;
Printer();
Printer(const Printer& rhs); //我们不定义这个函数
}; //因为不允许
//进行拷贝
// Obligatory definition of class static
size_t Printer::numObjects = 0;
Printer::Printer()
{
if (numObjects >= 1) {
throw TooManyObjects();
}
继续运行正常的构造函数;
++numObjects;
}
Printer * Printer::makePrinter()
{ return new Printer; }
当需要的对象过多时,会抛出异常,如果你认为这种方式给你的感觉是unreasonably
harsh,你可以让伪构造函数返回一个空指针。当然用户在使用之前应该进行检测。
除了用户必须调用伪构造函数,而不是真正的构造函数之外,它们使用Printer类就象
使用其他类一样:
Printer p1; // 错误! 缺省构造函数是private
Printer *p2 =
Printer::makePrinter(); // 正确, 间接调用缺省构造函数
Printer p3 = *p2; // 错误! 拷贝构造函数是private
p2->performSelfTest(); // 所有其它的函数都可以
p2->reset(); // 正常调用
...
delete p2; // 避免内存泄漏,如果
// p2 是一个 auto_ptr,
// 就不需要这步。
这种技术很容易推广到限制对象为任何数量上。我们只需把hard-wired常量值1改为
根据某个类而确定的数量,然后消除拷贝对象的约束。例如,下面这个经过修改的Printer类的代码实现,最多允许10个Printer对象存在:
class Printer {
public:
class TooManyObjects{};
// 伪构造函数
static Printer * makePrinter();
static Printer * makePrinter(const Printer& rhs);
...
private:
static size_t numObjects;
static const size_t maxObjects = 10; // 见下面解释
Printer();
Printer(const Printer& rhs);
};
// Obligatory definitions of class statics
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); }
如果你的编译器不能编译上述类中Printer::maxObjects的声明,这丝毫也不奇怪。特
别是应该做好准备, 编译器不能编译把10做为初值赋给这个变量这条语句。 给static const成员(例如int, char, enum等等)确定初值的功能是最近才加入到C++中的,所以一些编译器还不允许这样编写。如果没有及时更新你的编译器,可以把maxObjects声明为在一个private内匿名枚举类型里的枚举元素,
class Printer {
private:
enum { maxObjects = 10 }; // 在类中,
... // maxObjects为常量10
};
或者象non-const static成员一样初始化static常量:
class Printer {
private:
static const size_t maxObjects; // 没有赋给初值
...
};
// 放在一个代码实现的文件中
const size_t Printer::maxObjects = 10;
后面这种方法与原来的方法有一样的效果, 但是显示地确定初值能让其他程序员更容易理解。当你的编译器支持在类定义中给const static成员赋初值的功能时,你应该尽可能地利用这个功能。
一个具有对象计数功能的基类
如果我们有大量像Printer需要限制实例数量的类,就必须一遍又一遍地编写一样的代码,每个类编写一次。这将会使大脑变得麻木。应该有一种方法能够自动处理这些事情。难道没有方法把实例计数的思想封装在一个类里吗?
我们很容易地能够编写一个具有实例计数功能的基类, 然后让像Printer这样的类从该
基类继承,而且我们能做得更好。
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(); // 避免构造函数的
}; // 代码重复
template<class BeingCounted>
Counted<BeingCounted>::Counted()
{ init(); }
template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
{ init(); }
template<class BeingCounted>
void Counted<BeingCounted>::init()
{
if (numObjects >= maxObjects) throw TooManyObjects();
++numObjects;
}
从这个模板生成的类仅仅能被做为基类使用,因此构造函数和析构函数被声明为
protected。注意private成员函数init用来避免两个Counted构造函数的语句重复。
现在我们能修改Printer类
class Printer: private Counted<Printer> {
public:
// 伪构造函数
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使用了Counter模板来跟踪存在多少Printer对象,坦率地说,除了Printer
的编写者,没有人关心这个事实。它的实现细节最好是 private,这就是为什么这里使用
private继承的原因。另一种方法是在Printer和counted<Printer>之间使用public继承,但是我们必须给Counted类一个虚拟析构函数。(否则如果有人通过Counted<Printer>*指针删除一个Printer对象,我们就有导致对象行为不正确的风险)。在Counted中存在虚函数,几乎肯定影响从Counted继承下来的对象的大小和布局。我们不想引入这些额外的负担,所以使用private继承来避免这些负担。
Counted所做的大部分工作对于Printer的用户来说都是隐藏的,但是这些用户可能很
想知道有当前多少Printer对象存在。Counted模板提供了objectCount函数,用来提供这
种信息,但是因为我们使用private继承,这个函数在Printer类中成为了private。为了
恢复该函数的public访问权,我们使用using声明:
class Printer: private Counted<Printer> {
public:
...
using Counted<Printer>::objectCount; // 让这个函数对于Printer
//是public
...
};
这样做是合乎语法规则的,但是如果你的编译器不支持命名空间,编译器就不允许这样做。如果这样的话,你应使用老式的访问权声明语法:
class Printer: private Counted<Printer> {
public:
...
Counted<Printer>::objectCount; // 让objectCount
// 在Printer中是public
...
};
这种更传统的语法与uning声明具有相同的含义。但是我们不赞成这样做。
TooManyObjects类应该也应用同样的方式来处理,因为Printer的客户端如果要捕获这种
异常类型,它们必须有能力访问TooManyObjects。当Printer继承Counted<Printer>时,
它可以忘记有关对象计数的事情。编写Printer类时根本不用考虑对象计数,就好像有其他人会为它计数一样。
Printer的构造函数可以是这样的: Printer::Printer()
{
进行正常的构造函数运行
}
这里有趣的不是你所见到的东西,而是你看不到的东西。不检测对象的数量就好像限制
将被超过,执行完构造函数后也不增加存在对象的数目。所有这些现在都是由Counted<Printer>的构造函数来处理,因为Counted<Printer>是Printer的基类,我们知
道Counted<Printer>的构造函数总在 Printer 的前面被调用。如果建立过多的对象,
Counted<Printer>的构造函数就会抛出异常,甚至都没有调用Printer的构造函数。
最后还有一点需要注意,必须定义Counted内的静态成员。对于numObjects来说,这
很容易——我们只需要在Counted的实现文件里定义它即可:
template<class BeingCounted> // 定义numObjects
int Counted<BeingCounted>::numObjects; // 自动把它初始化为0
对于maxObjects来说,则有一些技巧。我们应该把它初始化为什么值呢?如果你想允许建立10个printer对象,我们应该初始化Counted<Printer>::maxObjects为10。另一方面如果我们向允许建立16个文件描述符对象,我们应该初始化Counted<Printer>::maxObjects为16。到底应该怎么做呢?
简单的方法就是什么也不做。我们不对maxObject进行初始化。而是让此类的客户端提供合适的初始化。
Printer的作者必须把这条语句加入到一个实现文件里:
const size_t Counted<Printer>::maxObjects = 10;
同样FileDescriptor的作者也得加入这条语句:
const size_t Counted<FileDescriptor>::maxObjects = 16;
如果这些作者忘了对maxObjects进行初始化,会发生什么情况呢?很简单:连接时会
发生错误,因为maxObjects没有被定义。如果我们提供了充分的文档对Counted客户端说明了需求,他们会回去加上这个必须的初始化。