您的位置:首页 > 编程语言

第五章 面向对象的编程风格(定义一个抽象基类)

2015-05-23 10:41 253 查看
5.4定义一个抽象基类

本节将重新定义前一节的num_sequence class.我要为所有数列设计出共享的抽象基类,然后继承它。

这该如何做到呢? 定义抽象基类的第一个步骤就是找出所有子类共通的操作行为。

举个列子,所有数列类的共通操作行为是什么呢?这些操作行为代表的是num_sequence这个基类的公有接口(public interface)。

以下是我的第一次尝试:

class num_sequesce{

public:

//elem(pos):返回位置上的元素 int elem(int pos)

//gen_elems(pos):产生直到位置的所有元素void gen_elems(int pos)

//what_am_i():返回确认的数列类型const char* what_am_i()const

//print(os):将所有元素写入ostream& print(ostream &os=cout)const

//check_integrity(pos)检查是否为有效位置bool _integrity(int pos)

//max_elems():返回支持的最大位置值static int max_elems()

int elem(int pos);

void gen_elems(int pos);

const *char what_am_i()const;

ostream& print(ostream &os=cout)const;

bool check_integrity(int pos);

static int max_elems();//不需要实例,直接调用

//...

elem()会返回用户指定的位置上的元素值。

max_elems()会返回最大的元素个数。

check_integrity()会判断传入的是否为有效位置

print()会输出元素值

gen_elems()会产生数列元素

what_am_i()会返回一个字符串,代表数列的名称。

设计抽象基类的下一步,便是设法找出哪些操作行为与类型相关(type_dependent)——也就是说,由哪些操作行为必须根据不同的派生类而有不同的实现方式,这些操作行为应该成为整个类继承体系中的虚函数。

以本例而言,每个数列都必须提供它们自己的gen_elems()实现,但check_integrity()就不会因为类型不同而有任何差异——它必须能够判断pos位置是否有效而其判断方式并不因数列类型而有所不同。同样的,max_elems()也不会因类型不同而有任何的差异——所有数列类型都可以存储着同样的元素。

并非每一个函数都能如此轻易的做出区分。what_am_i()也许不会和类型相关——这和我们的继承体系的实现有关。对elem()和print()来说,情况也是如此,此刻我们先假设它们都和类型相关。稍后我们将看到另一种截然不同的设计,将它们转换为和类型无关的函数。注意,static member function无法被声明为虚拟函数。

抽象基类设计的第三步,便是试着找出每个操作行为的访问层级。如果某个操作行为应该让一般程序皆能访问,我们应该将它声明为public.例如elem(),max_elems(),what_am_i()但如果某个操作行为在基类之外不需要被用到,我们就将它声明为private.即使是该基类的派生类型,亦无法访问基类中的private member.本例的所有操作行为都必须给派生类用,所以我们不能把它们声明为private.

第三种访问层级,是所谓的protected.这种层级的操作行为可以让派生类访问,却不允许一般程序使用。例如check_integrity() 和gen_elems()都是派生类必须调用的,却不是一般程序会用到的。以下便是重新修正之后的

num_sequence class的定义:

class num_sequence{

public:

virtual ~num_sequence(){};

virtual int elem(int pos)const=0;

virtual const char* what_am_i()const=0;

static int max_elems(){return _max_elems;}

virtual ostream& print(ostream &os=cout)const=0;

protected:

virtual void gen_elems(int pos)const=0;

bool check_integrity(int pos)const=0;

const static int _max_elems=1024;

};//对数据成员分类的一般规律:1、public\private\protected 2、对virtual进行分类 3、构造继承函数。

每个虚函数,要么得有其定义,要么可设为纯虚函数——如果对于该类而言,这个虚函数并无实际意义的话。

例如:gen_elems()之于num_sequence class.将虚函数赋值为0,意思是另它成为一个纯虚函数:

virtual void gen_elems(int pos)=0;

任何类声明如果有一个或多个纯虚函数,那么,由于其接口的不完整性(纯虚函数没有函数的定义,是谓不完整),程序无法为它产生任何对象。这种类只能作为派生类的子对象(subject)使用,而且前提是这些派生类必须为所有的虚函数提供确切的定义num_sequence class应该声明什么样的data member呢?这个问题并不存在任何必须坚持或者快速判断的标准。本例中,num_sequence并未声明任何data
member,因为它只是为数列继承体系提供的一个接口;派生类必须自行设计自身的data member.

那么constructor和destructor又当如何?由于此类并没有任何non_static data member需要进行初始化操作,所以其constructor亦无存在价值。不过,我会为它设计destructor。是的,根据一般规则,凡基类定义有一个或多个虚函数,应该将其destructor声明为virtual.像这样:

class num_sequence{

public:

virtual~destructor();

//...

}

为什么呢?考虑已下程序片段:

num_sequence *ps=new Fibonacci(12);

//....使用数列

delete ps;

ps是基类num_sequence的指针,但它实际是指向派生类fbonacci对象。当Delete表达式被应用于该指针,destructor会先应用于指针所指的对象上,于是将此对象占用的内存空间归还给程序的空闲(free store)。还记得吗?non_virtual函数在编译时便已经完成了解析(resolved)根据该对象被调用的类型来判断。

于是,本例中,通过ps调用的destructor一定是fibonacci的destructor.正确的情况应该是根据实际对象的类型选择调用哪一个destructor。而解析操作应该在运行时进行。为了促进正确的行为的发生,我们必须将destructor声明为virtual。

但是我并不建议在我们这个基类将其destructor声明为pure virtual——虽然它其实不具有任何实质意义的实现内容。对这类destructor而言,最好是提供空白定义,像下面这样2:

inline num_sequence::~num_sequence(){}

为求完整,下面列出num_sequence的output运算符和check_integrity()函数的实现:

bool num_sequence::

check_integrity(int pos)const

{

if(pos<=0||pos>_max_elems)

{

cerr<<"!!invalid position:"<<pos<<"cannot honor request\n";

return false;

}

return true;

}

ostream& operator<<(ostream &os,const num_sequence &ns)

{return ns.print(os);}

虽然上述内容使我们完成了抽象基类num_sequence的整个定义,但是类本身并不完全,它仅仅是为其派生类提供了一个接口而已。每个派生类还必须提供适合的实现细节,以补足基类num_sequence的不足。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐