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

前言

 
本节内容的极端情况即为禁止建立对象与单例模式。


禁止对象实例化

 
如果我们需要阻止建立某个类的对象,应该把它的构造函数设为private:

1
2
3
4
5
6
class CantBeInstantiated {
private:
CantBeInstantiated();
CantBeInstantiated(const CantBeInstantiated&);
...
};


单例模式

 
假设我们现在只允许生成一个打印机对象,我们应当把这个对象封装置入某个函数,允许所有人访问,但只有一个对象存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

这个设计有三部分:

  1. printer无法构造
  2. 全局函数thePrinter被设为friend,因此可以调用private构造函数。
  3. thePrinter包含一个静态Printer对象,这意味着只有一个对象被建立。我们只能通过thePrinter函数来使用这个对象。

静态函数

有人会认为thePrinter没必要污染全局命名空间,为此解决方法是把thePrinter声明为printer内部的静态函数,那顺便去除了friend属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Printer{
public:
static Printer& thePrinter();
...
private:
Printer();
Printer(const Printer& rhs);
...
};
Printer& Printer::thePrinter(){
static Printer p;
return p;
}

就是调用函数的时候麻烦了一些:
1
2
Printer::thePrinter().reset();
Printer::thePrinter().submitJob(buffer);

新建命名空间

另一种做法是将class与thePrinter移出全局域,放入专属namespace,防止命名冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace PrintingStuff{
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;
}

只不过这样一来调用每一次调用都必须提及命名空间:
1
2
PrintingStuff::thePrinter().reset();
PrintingStuff::thePrinter().submitJob(buffer);

又或者使用using:
1
2
3
using PrintingStuff::thePrinter; 
thePrinter().reset();
thePrinter().submitJob(buffer);


单例模式实现细节

thePrinter的实现有两个微妙之处:

  1. p是函数中的静态对象而非类中的静态对象
    如果是类中的静态对象,则该对象总是会被构造和析构。(即使你根本没有试图去使用)函数中的则不同,只有第一次调用函数时才会被构造。当然,我们也为此付出了代价:每一次都必须检查是否需要建立对象。
    如果对象是类中的静态成员,它的初始化时间难以确定,我们清楚的了解函数的静态对象初始化于函数的第一次调用,而类则说不准。
  2. 关注内联与函数内静态对象的关系
    我们看得出thePrinter的函数体非常简短,很适合内联,但我们不能内联它。因为对于非成员函数,内联会导致复制obj中的代码,简单来说,就连静态对象也被复制了。所以不要内联包含局部静态数据的非成员对象。

多例模式(限制数目)

 
一般来说,限制对象数目的核心思想在于:实时跟踪当前已生成的对象数目,如果超出则抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Printer {
public:
class TooManyObjects{};//异常类
Printer();
~Printer();
...
private:
static size_t numObjects;
Printer(const Printer& rhs);//禁止拷贝
};
size_t Printer::numObjects = 0;
Printer::Printer(){
if (numObjects >= 1) {
throw TooManyObjects();
}
...//执行构造
++numObjects;
}
Printer::~Printer(){
...//执行析构;
--numObjects;
}

这种方法相当直观,就是设计起来存在些许小小的问题。


建立对象的环境

假设我们有一个彩色打印机,继承自printer:

1
2
3
class ColorPrinter: public Printer {...};
Printer p;
ColorPrinter cp;

上述定义产生了两个Printer对象,于是,在构造cp的基类部分时抛出了异常(More Effective C++ 33提出设计时避免非尾端类为具象类)。
如果有其他对象包含Printer对象,也会有同样的问题:
1
2
3
4
5
6
7
8
9
class CPFMachine {
private:
Printer p;//有打印能力
FaxMachine f;//有传真能力
CopyMachine c;//有复印能力
...
};
CPFMachine m1;//运行正常
CPFMachine m2;//抛出异常

这些问题的根源在于Pointer对象可以在3种不同状态下生存:

  1. 仅有自身
  2. 作为派生对象的base成分
  3. 内嵌于较大对象

这直接导致了我们心目中的“目标个数”与编译器看到的目标个数不一致。

private构造函数

通常情况下我们只对状态1感兴趣,那把构造函数设为private可以很有效地满足我们。带有private构造函数的类无法作为基类,也无法内嵌到其他对象中。
private构造函数是一种阻止产生派生类的好手段,具体来说,假如你有一个类FSA(finite state automata),我们允许它产生多个对象,但禁止从它派生出新类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FSA {
public:
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函数总是返回一个对象的引用(引用的对象是固定的),每个 makeFSA的伪构造函数则是返回一个指向对象的指针,也就是说允许建立的 FSA 对象数量没有限制。
为了防止资源泄漏,最佳方案自然是RAII:
1
2
auto_ptr<FSA> pfsa1(FSA::makeFSA());
auto_ptr<FSA> pfsa2(FSA::makeFSA(*pfsa1));


允许对象自由生灭

在之前的设计手法中,首次我们调用thePrinter时,对象被构造,但我们无法控制对象的销毁,也就是说无法达成这样的功能:

1
2
3
4
5
6
建立Printer对象 p1;
使用p1;
释放p1;
建立Printer对象 p2;
使用p2;
释放p2;

解决方法就是把对象计数和伪构造函数合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
};
size_t Printer::numObjects = 0;
Printer::Printer(){
if (numObjects >= 1) {
throw TooManyObjects();
}
...//执行构造
++numObjects;
}
Printer::~Printer(){
...//执行析构;
--numObjects;
}

除了用户必须调用伪构造函数之外,这一切使用起来就如同其他类:
1
2
3
4
5
6
7
Printer p1;//error 构造函数为private
Printer *p2 =Printer::makePrinter();
Printer p3 = *p2; //error 禁止拷贝
p2->performSelfTest();
p2->reset();
...
delete p2;//避免资源泄漏

如果我们需要制定最多生成N个,那只要在class中定义一个static const maxsize=N就ok:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;//内置static类型可以直接在class中声明时定义
Printer();
Printer(const Printer& rhs);
};
size_t Printer::numObjects = 0;//在类外定义
const size_t Printer::maxObjects;//定义


建立具有计数功能的基类

如果我们需要大量这种限制产出对象数量的class,难道我们要一遍一遍地重复编写?当然不是。
理想做法是编写一个具有实例计数功能的基类,然后让产出受限的类继承该类。我们使用一种方法封装计数功能,这种做法不但封装维护实例计数器的函数,也封装实例计数器本身。
下面给出该类的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}

于是最终的Printer类如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
};

用户有权得知当前的对象数目以及最大数目,private继承使得它们被隐藏,因此需要在public中使用using来获取他们。
最后需要注意的就是定义counted内部的静态成员,对于numobject,我们只需要在counted的实现中定义:
1
2
template<class BeingCounted>
int Counted<BeingCounted>::numObjects;//自动初始化为0

max的定义则略微麻烦,我们不初始化它,而是留待客户进行初始化。以Printer举例:
1
const size_t Counted<Printer>::maxObjects = 10;

如果客户没写,那么在连接时会报错。