More Effective C++ 读书摘要(五、技巧1)Item25 - 27

来源:互联网 发布:php wind html5 编辑:程序博客网 时间:2024/05/02 01:56

 Item25. 使构造函数和非成员函数具有虚函数的行为:


有三个类,注意其继承关系:

其构造函数如下:

readComponent所做的工作是:它根据所读取的数据建立了一个新对象,或是TextBlock或是Graphic。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数。虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。

 

还有一种特殊种类的虚拟构造函数――虚拟拷贝构造函数,也有着广泛的用途。虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝:

 

类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。因此”拷贝”的含义与真正的拷贝构造函数相同(浅拷贝或深拷贝、是否引用计数)。注意上述代码的实现利用了最近才被采纳的较宽松的虚拟函数返回值类型规则。被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。

 

NLComponent有了虚拷贝构造函数以后,要为NewLetter实现一个的(常规的)拷贝构造函数变得很容易:

 

让非成员函数具有虚函数的行为

具有虚拟行为的非成员函数很简单。你编写一个虚拟函数来完成工作,然后再写一个非虚拟函数,它什么也不做只是调用这个虚拟函数。为了避免这个句法花招引起函数调用开销,你可以内联这个非虚函数

 

Item26. 限制类对象的个数:


允许一个或零个对象

 

客户端使用printer时有些繁琐:
Printer::thePrinter().reset();
Printer::thePrinter().submitJob(buffer);

 

另一种办法是把thePrinter移出全局域,放入namespace(命名空间)

在thePrinter的实现上有两个微妙的不引人注目的地方,值得我们看一看。

 

第一,单独的Printer是位于函数里的静态成员而不是在类中的静态成员,这样做是非常重要的。在类中的一个静态对象实际上总是被构造(和释放),即使不使用该对象。与此相反,只有第一次执行函数时,才会建立函数中的静态对象,所以如果没有调用函数,就不会建立对象。(不过你得为此付出代价,每次调用函数时都得检查是否需要建立对象。)与一个函数的静态成员相比,把Printer声明为类中的静态成员还有一个缺点,它的初始化时间不确定

 

第二个细微之处是内联与函数内静态对象的关系。不要建立包含局部静态数据的非成员内联函数。
也可以只需简单地计算对象的数目,一旦需要太多的对象,就抛出异常,这样做也许会更好。如下所示,这样建立printer对象:

以下为实现:

 

不同情形下的对象创建
刚刚的Printer类在以下的情形下会有问题。  

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

 

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

 

你不能从带有private构造函数的类派生出新类,这个事实衍生出了一种阻止派生类的通用方法,这种方法不需要和限制对象实例数量的方法一起使用。

 

现在考虑有一个类FSA,表示一个finite state automata(有限态自动机) 。(这种机器能用于很多环境下,比如用户界面设计),并假设你允许建立任意数量的对象,但是你想禁止从FSA派生出新类。(这样做的一个原因是表明在FSA中存在非虚析构函数。) 

可以把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,
                           //不过不用操心删除它们。 

 

允许对象来去自由

如前的方案使用thePrinter函数封装对单个对象的访问,以便把Printer对象的数量限制为一个,这样做的同时也会让我们在每一次运行程序时只能使用一个Printer对象。即不能编写以下的代码:

(thy:因为thePrinter管理的是一个静态对象。)

 

我们需要做的就是把先前使用的对象计数的代码与makePrinter()伪构造函数代码合并在一起:

 

一个用于对象计数的基类 

 

现在能使用Counted模板修改Printer类:

Printer使用了Counter模板来跟踪存在多少Printer对象,因为除了Printer的编写者,没有人关心这个事实。它的实现细节最好是private,这就是为什么这里使用private继承的原因。

 

Counted所做的大部分工作对于Printer的客户端来说都是隐藏的,但是这些客户端可能很想知道有当前多少Printer对象存在。Counted模板提供了objectCount函数,用来提供这种信息,但是因为我们使用private继承,这个函数在Printer类中成为了private。为了恢复该函数的public访问权,我们使用using声明:  

这里没有检测对象的数量是否已经超过限制,执行完构造函数后也没有增加存在对象的数目。所有这些现在都由Counted<Printer>的构造函数来处理,因为Counted<Printer>是Printer的基类,Counted<Printer>的构造函数总在Printer的前面被调用。如果建立过多的对象,Counted<Printer>的构造函数就会抛出异常,甚至都没有调用Printer的构造函数。

 

最后一个问题是应该初始化Counted<Printer>::maxObjects为多少呢?
简单的方法就是什么也不做。而是让此类的客户端提供合适的初始化。Printer的作者必须把这条语句加入到一个实现文件里:
const size_t Counted<Printer>::maxObjects = 10;
如果这些作者忘了对maxObjects进行初始化,连接时就会发生错误,因为maxObjects没有被定义。如果我们提供了充分的文档对Counted客户端说明了需求,他们会回去加上这个必须的初始化。

 

Item27. 要求或禁止对象分配在堆上:

 

要求在堆中建立对象

一种最直接的方法是让析构函数成为private,让构造函数成为public。通过引入一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。

然后客户端这样进行程序设计:

通过限制访问一个类的析构函数或它的构造函数来阻止建立非堆对象,但是在条款26已经说过,这种方法也禁止了继承和包含(containment)。

 

通过把UPNumber的析构函数声明为protected(同时它的构造函数还保持public)就可以解决继承的问题,需要包含UPNumber对象的类可以修改为包含指向UPNumber的指针:

 

 

判断对象是否在堆中

 如果想利用一个在很多系统上存在的事实,程序的地址空间被做为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展,来判断某个特定的地址是否在堆中:

到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别。


令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。(作者后来确信基于签名的技巧是一种直接明了的方法:http://www.aristeia.com/BookErrata/M27Comments.html)

 

其实研究对象是否在堆中这个问题,一个可能的原因是你想知道对象是否能在其上安全调用delete。幸运的是“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易一些。

 

我们希望这些函数提供这些功能时能够不污染全局命名空间,没有额外的开销,没有正确性问题。幸运的是C++使用一种抽象混合(mixin)基类满足了我们的需要。


抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。mixin(mix in)类提供某一特定的功能,并可以与其继承类提供的其它功能相兼容)。这种类几乎都是抽象类。因此我们能够使用抽象混合(mixin)基类给派生类提供判断指针指向的内存是否由operator new分配的能力。该类如下所示: 

下面是其实现:

代码还是很一目了然。只有一个地方可能让你感到困惑,就是这个语句(在isOnHeap函数中)
const void *rawAddress = dynamic_cast<const void*>(this);
因为带有多继承或虚基类的对象会有几个地址,这导致编写全局函数isSafeToDelete会很复杂。这个问题在isOnHeap中仍然会遇到,但是因为isOnHeap仅仅用于HeapTracked对象中,我们能使用dynamic_cast操作符的一种特殊的特性来消除这个问题。只需简单地放入dynamic_cast,把一个指针dynamic_cast成void*类型(或const void*或volatile void* 。。。。。),生成的指针指向“原指针指向对象内存”的开始处。如果你的编译器支持dynamic_cast 操作符,这个技巧是完全可移植的。

 

使用这个类,即使是最初级的程序员也可以在类中加入跟踪堆中指针的功能。他们所需要做的就是让他们的类从HeapTracked继承下来。例如我们想判断Assert对象指针指向的是否是堆对象:  

我们能够这样查询Assert*指针,如下所示:

 

 

禁止对象分配在堆上

通常对象的建立这样三种情况:对象被直接实例化;对象做为派生类的基类被实例化;对象被嵌入到其它对象内。以下分别说明:

 

①禁止用户直接实例化对象:
可以这样编写:

现在客户端仅仅可以做允许它们做的事情:

UPNumber n1;                                  // okay

static UPNumber n2;                         // also okay

UPNumber *p = new UPNumber;      // error! attempt to call
                                                        // private operator new

如果你也想禁止UPNumber堆对象数组,可以把operator new[]和operator delete[](参见条款8)也声明为private。

 

②把operator new声明为private同样会阻碍UPNumber对象做为一个位于堆中的派生类对象的基类被实例化。因为如果operator new和operator delete没有在派生类中被声明为public,它们就会被继承下来,继承了基类private函数的类,如下所示:

 

③同样,UPNumber的operator new是private这一点,不会对分配包含做为成员的UPNumber对象的对象产生任何影响:
  

原创粉丝点击