分享
 
 
 

More Effective C++ 条款26(上)

王朝c/c++·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

条款26:限制某个类所能产生的对象数量(上)

你很痴迷于对象,但是有时你又想束缚住你的疯狂。例如你在系统中只有一台打印机,所以你想用某种方式把打印机对象数目限定为一个。或者你仅仅取得16个可分发出去的文件描述符,所以应该确保文件描述符对象存在的数目不能超过16个。你如何能够做到这些呢?如何去限制对象的数量呢?

如果这是一个用数学归纳法进行的证明,你会从n=1开始证明,然后从此出发推导出其它证明。幸运的是这既不是一个证明也不是一个归纳。而从n=0开始更具有启发性,所以我们就从这里开始。你如何能够彻底阻止对象实例化(instantiate)呢?

允许建立零个或一个对象

每次实例化一个对象时,我们很确切地知道一件事情:“将调用一个构造函数。”事实确实这样,阻止建立某个类的对象,最容易的方法就是把该类的构造函数声明在类的private域:

class CantBeInstantiated {

private:

CantBeInstantiated();

CantBeInstantiated(const CantBeInstantiated&);

...

;

这样做以后,每个人都没有权力建立对象,我们能够有选择性地放松这个限制。例如如果想为打印机建立类,但是要遵守我们只有一个对象可用的约束,我们应把打印机对象封装在一个函数内,以便让每个人都能访问打印机,但是只有一个打印机对象被建立。:

class PrintJob; // forward 声明

// 参见Effective C++条款34

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;

}

这个设计由三个部分组成,第一、Printer类的构造函数是private。这样品能阻止建立对象。第二、全局函数thePrinter被声明为类的友元,让thePrinter避免私有构造含函数引起的限制。最后thePrinter包含一个静态Printer对象,这意味着只有一个对象被建立。

客户端代码无论何时要与系统的打印机进行交互访问,它都要使用thePrinter函数:

class PrintJob {

public:

PrintJob(const string& whatToPrint);

...

};

string buffer;

... //填充buffer

thePrinter().reset();

thePrinter().submitJob(buffer);

当然你感到thePrinter使用全局命名空间完全是多余的。“是的” ,你会说,“全局函数看起来象全局变量,但是全局变量是gauche(不知如何翻译 译者注),我想把所有与打印有关的功能都放到Printer类里。”好的,我绝不敢与使用象gauche这样的词的人争论(有些不明白这里的意思译者注),这很简单,只要在Prihter类中声明thePrinter为静态函数,然后把它放在你想放的位置。就不再需要友元声明了。使用静态函数,如下所示:

class Printer {

public:

static Printer& thePrinter();

...

private:

Printer();

Printer(const Printer& rhs);

...

};

Printer& Printer::thePrinter()

{

static Printer p;

return p;

}

客户端使用printer时有些繁琐:

Printer::thePrinter().reset();

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

另一种方法是把thePrinter移出全局域,放入namespace(命名空间)(参见Effective C++条款28)。命名空间是C + +一个较新的特性。任何能在全局域声明东西也能在命名空间里声明。包括类、结构、函数、变量、对象、typedef等等。把它们放入命名空间并不影响它们的行为特性,不过能够防止在不同命名空间里的实体发生命名冲突。把Printer类和thePrinter函数放入一个命名空间,我们就不用担心别人也会使用Printer和thePrinter名字;命名空间能够防止命名冲突。

命名空间从句法上来看有些象类,但是它没有public、protected或private域。所有都是public。如下所示,我们把Printer、thePrinter放入叫做PrintingStuff的命名空间里:

namespace PrintingStuff {

class Printer { // 在命名空间

public: // PrintingStuff中的类

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;

}

}

// 命名空间到此结束

使用这个命名空间后,客户端可以通过使用fully-qualified name(完全限制符名)(即包括命名空间的名字),

PrintingStuff::thePrinter().reset();

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

但是也可以使用using声明,以简化键盘输入:

using PrintingStuff::thePrinter; // 从命名空间"PrintingStuff"

//引入名字"thePrinter"

// 使其成为当前域

thePrinter().reset(); // 现在可以象使用局部命名

thePrinter().submitJob(buffer); // 一样,使用thePrinter

在thePrinter的实现上有两个微妙的不引人注目的地方,值得我们看一看。第一,单独的Pritner是位于函数里的静态成员而不是在类中的静态成员,这样做是非常重要的。在类中的一个静态对象实际上总是被构造(和释放),即使不使用该对象。与此相反,只有第一次执行函数时,才会建立函数中的静态对象,所以如果没有调用函数,就不会建立对象。(不过你得为此付出代价,每次调用函数时都得检查是否需要建立对象。)建立C++一个理论支柱是你不需为你不用的东西而付出,在函数里,把类似于Printer这样的对象定义为静态成员就是坚持这样的理论。你应该尽可能坚持这种理论。

与一个函数的静态成员相比,把Printer声明为类中的静态成员还有一个缺点,它的初始化时间不确定。我们能够准确地知道函数的静态成员什么时候被初始化:“在第一次执行定义静态成员的函数时”。而没有定义一个类的静态成员被初始化的时间。C++为一个translation unit(也就是生成一个object文件的源代码的集合)内的静态成员的初始化顺序提供某种保证,但是对于在不同translation unit中的静态成员的初始化顺序则没有这种保证(参见Effective C++条款47)。在实际使用中,这会给我们带来许多麻烦。当函数的静态成员能够满足我们的需要时,我们就能避免这些麻烦。在这里的例子里,既然它能够满足需要,我们为什么不用它呢?

第二个细微之处是内联与函数内静态对象的关系。再看一下thePrinter的非成员函数形式:

Printer& thePrinter()

{

static Printer p;

return p;

}

除了第一次执行这个函数时(也就是构造p时),其它时候这就是一个一行函数——它由"return p;"一条语句组成。这个函数最适合做为内联函数使用。然而它不能被声明为内联。为什么呢?请想一想,为什么你要把对象声明为静态呢?通常是因为你只想要该对象的一个拷贝。现在再考虑“内联” 意味着什么呢?从概念上讲,它意味着编译器用函数体替代该对函数的每一个调用,不过非成员函数除外。非成员函数还有其它的含义。它还意味着internal linkage(内部链接)。

通常情况下,你不需要理解这种语言上令人迷惑的东西,你只需记住一件事:“带有内部链接的函数可能在程序内被复制(也就是说程序的目标(object)代码可能包含一个以上的内部链接函数的代码),这种复制也包括函数内的静态对象。”结果如何?如果建立一个包含局部静态对象的非成员函数,你可能会使程序的静态对象的拷贝超过一个!所以不要建立包含局部静态数据的非成员函数。

但是你可能认为建立函数来返回一个隐藏对象的引用,这种限制对象的数量的方法是错误的。也许你认为只需简单地计算对象的数目,一旦需要太多的对象,就抛出异常,这样做也许会更好。如下所示,这样建立printer对象,:

class Printer {

public:

class TooManyObjects{}; // 当需要的对象过多时

// 就使用这个异常类

Printer();

~Printer();

...

private:

static size_t numObjects;

Printer(const Printer& rhs); // 这里只能有一个printer,

// 所以不允许拷贝

}; // (参见Effective C++ 条款27)

此法的核心思想就是使用numObjects跟踪Pritner对象存在的数量。当构造类时,它的值就增加,释放类时,它的值就减少。如果试图构造过多的Printer对象,就会抛出一个TooManyObjects类型的异常:

// Obligatory definition of the class static

size_t Printer::numObjects = 0;

Printer::Printer()

{

if (numObjects >= 1) {

throw TooManyObjects();

}

继续运行正常的构造函数;

++numObjects;

}

Printer::~Printer()

{

进行正常的析构函数处理;

--numObjects;

}

这种限制建立对象数目的方法有两个较吸引人的优点。一个是它是直观的,每个人都能理解它的用途。另一个是很容易推广它的用途,可以允许建立对象最多的数量不是一,而是其它大于一的数字。

建立对象的环境

这种方法也有一个问题。假设我们一个特殊的打印机,是彩色打印机。这种打印机类有许多地方与普通的打印机类相同,所以我们从普通打印类继承下来:

class ColorPrinter: public Printer {

...

};

现在假设我们系统有一个普通打印机和一个彩色打印机:

Printer p;

ColorPrinter cp;

这两个定义会产生多少Pritner对象?答案是两个:一个是p,一个是cp。在运行时,当构造cp的基类部分时,会抛出TooManyObjects异常。对于许多程序员来说,这可不是他们所期望的事情。(设计时避免从其它的concrete类继承concrete类,就不会遇到这种问题。这种设计思想详见条款33)。

当其它对象包含Printer对象时,会发生同样的问题:

class CPFMachine { // 一种机器,可以复印,打印

private: // 发传真。

Printer p; // 有打印能力

FaxMachine f; // 有传真能力

CopyMachine c; // 有复印能力

...

};

CPFMachine m1; // 运行正常

CPFMachine m2; // 抛出 TooManyObjects异常

问题是Printer对象能存在于三种不同的环境中:只有它们本身;作为其它派生类的基类;被嵌入在更大的对象里。存在这些不同环境极大地混淆了跟踪“存在对象的数目” 的含义,因为你心目中的“对象的存在” 的含义与编译器不一致。

通常你仅会对允许对象本身存在的情况感兴趣,你希望限制这种实例(instantiation)的数量。如果你使用最初的Printer类示例的方法,就很容易进行这种限制,因为Printer构造函数是private,(不存在friend声明)带有private构造函数的类不能作为基类使用,也不能嵌入到其它对象中。

你不能从带有private构造函数的类派生出新类,这个事实导致产生了一种阻止派生类的通用方法,这种方法不需要和限制对象实例数量的方法一起使用。例如,你有一个类FSA,表示一个finite state automata(有限态自动机) 。(这种机器能用于很多环境下,比如用户界面设计),并假设你允许建立任意数量的对象,但是你想禁止从FSA派生出新类。(这样做的一个原因是表明在FSA中存在非虚析构函数。Effective C++ 条款14解释了为什么基类通常需要虚拟析构函数,本书条款24解释了为什么没有虚函数的类比同等的具有虚函数的类要小。)如下所示,这样设计FSA可以满足你的这两点需求:

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对象数量没有限制。

那好,不过每个伪构造函数都调用new这个事实暗示调用者必须记住调用delete。否则就会发生资源泄漏。如果调用者希望退出生存空间时delete会被自动调用,他可以把makeFSA返回的指针存储在auto_ptr中(参见条款9);当它们自己退出生存空间时,这种对象能自动地删除它们所指向的对象:

// 间接调用缺省FSA构造函数

auto_ptr<FSA> pfsa1(FSA::makeFSA());

// indirectly call FSA copy constructor

auto_ptr<FSA> pfsa2(FSA::makeFSA(*pfsa1));

... // 象通常的指针一样使用pfsa1和pfsa2,

//不过不用操心删除它们。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有