面向对象的设计(4)

来源:互联网 发布:linux文件系统详解 编辑:程序博客网 时间:2024/06/15 12:01

对于一个非虚拟函数的调用,编译器在编译时刻选择被调用的函数,而虚拟函数调用的

决定则要等到运行时刻。在执行程序内部的每个调用点上,系统根据被调用对象的实际基类

或派生类的类型来决定选择哪一个虚拟函数。实例,例如考虑下面的代码

void init( IntArray &ia )

{

for ( int ix = 0; ix < ia.size(); ++ix )

ia[ ix ] = ix;

}

形式参数ia 可以引用IntSortedArray、IntArrayRC 或IntArray 类的对象。我们将简要介

绍这里的派生类函数,size()作为非虚拟函数由编译器处理并内联展开,但是下标操

作符要直到执行循环的每次迭代时才能被处理,因为在编译期间编译器不知道数组ia 指向的

实际类型。

第17 章将详细讨论虚拟函数,包括虚拟析构函数的主题以及使用虚拟函数设计带来

的效率问题,【LIPPMAN96a 】对虚拟函数的实现与效率有更深入的讨论。

一旦我们定好了设计方案,C++的实现就很容易了。例如下面这个完整的IntArrayRC

派生类定义被放在一个独立的头文件IntArrayRC.h 中,该文件包含头文件IntArray.h, 而

IntArray.h 包含有IntArray 类的定义。

#ifndef IntArrayRC_H

#define IntArrayRC_H

#include "IntArray.h"

class IntArrayRC : public IntArray {

public:

IntArrayRC( int sz = DefaultArraySize );

IntArrayRC( int *array, int array_size );

IntArrayRC( const IntArrayRC &rhs );

virtual int& operator[]( int );

private:

void check_range( int );

};

#endif

IntArrayRC 只需定义不同于IntArray 实现的那些方面,或者加上对IntArray 扩展的实现。

1 它必须提供自己的下标操作符实例,以支持范围检查

2 它必须提供一个操作来做实际的检查工作,由于它不是公有接口的一部分,所以我们

把它声明为private

3 它必须提供一组自动初始化函数,即自己的构造函数集。

 

IntArray 的成员函数与数据成员对于IntArrayRC 来说都是可用的,就如同IntArrayRC

已经显式地定义了它们一样。这正是下面这句话的含义

class IntArrayRC : public IntArray

冒号定义了IntArrayRC 是从IntArray 派生而来的。关键字public 表明派生类共享基类

的公有接口。IntArrayRC 类型的对象可以用在任何可以使用基类类型对象的位置上。比

如在swap()例子中。第18 章会详细解释这一点。IntArrayRC 可以看作是IntArray 的扩展。

它增加了下标范围检查的额外特性。下面是下标操作符的一个实现

inline int&

IntArrayRC::operator[]( int index )

{

check_range( index );

return ia[ index ];

}

这里check_range()被实现为一个内联成员函数,它调用assert()宏。关于assert()宏的讨

论见1.3 节

#include <cassert>

inline void

IntArrayRC::check_range( int index )

{

assert( index >= 0 && index < size );

}

我们把check_range()函数作为一个独立的函数,以便说明私有成员函数并且将范围检

查的处理封装起来,方便我们以后改变边界错误的处理方式或是用异常处理代替assert()。

派生类对象实际上由几部分构成,每个基类是一个类的子对象subobject,它在新定

义的派生类中有独立的一部分派生类。对象的初始化过程是这样的:首先自动调用每个基类的构造函数来初始化相关的基类子对象,然后再执行派生类的构造函数。从设计的角度来看,派生类的构造函数应该只初始化那些在派生类中被定义的数据成员,而不是基类中的数据成员。

 

虽然我们引入了与类相关的下标操作符版本,以及一个私有的check_range()辅助函数,

但是我们并没有引入需要初始化的额外数据成员,因此,我们可以合理地假设.继承基类的

构造函数已经足够了,我们不需要再提供IntArrayRC 的构造函数——因为不需要它们做任何事情。

但是,实际上我们还是需要提供IntArrayRC 的构造函数,因为基类的构造函数并没有

被派生类继承,析构函数和拷贝赋值操作符同样也没有,还因为我们需要某个接口以便

通过这个接口把必要的参数传递给基类IntArray 的构造函数。

 

例如,假设我们定义了一个IntArrayRC 对象

int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13 };IntArrayRC iarc( ia, 8 );

怎样才能把ia 和8 传递给基类的构造函数呢?不可否认,如果IntArray 构造函数被继承

了,那么就没有这个问题。实际上,那样的话我们会有其他更严重的问题,但现在没有足够的

篇幅向你证明这一点,无论如何派生类构造函数的语法提供了向基类构造函数传递参数的

接口。

 

例如,下面是两个必需的IntArrayRC 构造函数,第14 章与第17 章将对构造函数作更

多的讲解。其中,包括关于为什么我们不需要提供IntArrayRC 拷贝构造函数的解释

inline IntArrayRC::IntArrayRC( int sz)

: IntArray( sz ) {}

inline IntArrayRC::IntArrayRC( const int *iar, int sz )

: IntArray( iar, sz ) {}

由冒号分割出来的部分称作成员初始化列表member initialization list,它提供了一种

机制,通过这种机制,我们可以向IntArray 的构造函数传递参数。两个IntArrayRC 构造函数

的函数体都是空的,因为它们的工作就是把参数传递给相关的IntArray 构造函数,我们无需

提供显式的IntArrayRC 析构函数,因为派生类没有引入任何需要析构的数据成员,继承过来

的需要析构的IntArray 成员都由IntArray 的析构函数来处理。

原创粉丝点击