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

5.5定义一个派生类

 

派生类由两部分组成:一是基类构成的子对象(subject),由基类的non-static data member如果有的话——组成,二是派生类的部分(由派生类的non-static data member组成).派生类的这种合成本质忠实的反映在了其声明的语法上:
//头文件"num_sequence.h"含有基类的定义

#include"num_sequence.h"
class fibonacci:public num_sequence
{
public:
//...
};

派生类的名称之后紧跟着冒号,关键字public,以及基类的名称。唯一的规则是,类进行继承声明之前,其基类的定义必须已经存在(这也是必须先行包含有num_sequence类定义头文件的原因)。fibonacci class必须为从基类继承而来的每个纯虚函数提供对应的实现。除此以外,它还必须声明fibonacci class专属的member.以下便是fibonacci class的定义:

class fibonacci:public num_sequence{
public:
fibonacci(int len=1;int beg_pos=1):_length,_beg_pos(beg_pos){}
virtual int elem(int pos)const;
virtual const char* what_am_i()const{return"fibonacci";}
virtual ostream& print(ostream&os=cout)const;
int length()const{return_length;}
int beg_pos const{return _beg_pos;}
protected:
virtual void gen_elems(int pos)const;
int _length;
int _beg_pos;
static vector<int>_elems;
}

在我的设计中,每个派生类都有长度和起始位置这两项data member。length()和beg_pos()这两个函数被声明为non-virtual,因为它们并无基类所提供的实体可以覆盖。但因为它们并非基类提供的接口的一员,所以当我们通过基类的pointer或是reference进行操作时,无法访问它们。例如:

//ok:通过虚函数机制,调用了fibonacci::what_am_i()
ps->what_am_i();


//ok:调用继承而来的num_sequence::max_elems()
ps->max_elems();


//错误:lengtn()而并非 num_sequence接口中的一员
ps->length();


//ok:通过虚函数机制调用 fibonacci destructor
delete ps;

 

如果“通过基类的接口无法访问length()和beg_pos()”会对我们产生困扰,那么我们应该回过头去修改基类的接口,重新设计的两方式之一便是在基类num_sequence内加上两个纯虚函数length()和beg_pos()。这样一来便会自动造成派生类的beg_pos()和length()都成为虚拟函数——它们不再需要指定关键字virtual。如果必须加上关键字virtual,那么修改基类的虚拟函数(例如beg_pos())就得大费周章:每个派生类都必须对它重新声明。

  另一种重新设计的方式是,将存储长度和起始位置的空间,由派生类抽离出来,移到基类。于是length()和beg_pos()都成了继承而来的inline nonvirtual function。5.6节会讨论这种设计方式的一个变形。
 

谈到这里,我的看法是,当我们面临“萃取基类和派生类之间的性质,以决定哪些东西(包括接口和实际成员)”属于谁时,面向对象设计所面对的挑战,将不仅是编程方面而已。一般而言,这是一个不断迭代的过程,过程之中借着程序员的经验和用户反馈,不断的演进。


  以下便是elem()、gen_elems()、print()的实现

 

派生类的虚函数必须精确吻合基类中的函数原型。在类之外对虚函数进行定义时,不必指明关键字virtual.
int fibonacci::elem(int pos)const
{
if(!check_integrity(pos))
return0;
if(pos>_elem.size())
fibonacci::gen_elems(pos);
return_elem[pos-1];
}


请注意,elem()调用继承来的check_integrity(),其形式仿佛后者为自身成员一般。一般来说,继承而来的public成员和protected成员,不论在继承体系中的深度如何,都可被视为派生类自身所拥有的成员。基类的public member在派生类中同样也是public,同样开放给派生类的用户使用。基类的protected member在派生类中同样也是protected,同样只能给后续的派生类使用,无法给目前这个派生类的用户使用,至于基类的private member,则完全无法让派生类使用。

 

请记住,在返回pos位置上的元素前,我们会检查_elems()拥有的元素是否够多。如果不够,elem()会调用gen_elems(),计算必要的元素并填入_elems()。这个操作必须写成fibonacci::gen_elems(pos),不能只是简单地写gen_elems(pos)。想知道为什么吗?这可是一个好问题。
  在elem()内,我们清楚的知道我们想调用的究竟是哪一个gen_elems(),在fibonacci::elem()中我们想调用的是fibonacci::gen_elems(),明确的很,不必等到运行才进行gen_elems()的解析操作,事实上,我们希望跳过虚函数机制,使该函数在编译就完成解析,不必等到运行时才解析。这就是我们指明调用对象的原因,通过运算符,我们可以明确的告诉编译器,我们想调用那一份函数实例。于是,运行时发生的虚拟机制便被遮掩了。
以下是gen_elems()和print()的实现:

void fibonmacci::gen_elems(int pos) const

{

if(_elems.empty())

{_elems.push_back(1);

_elems.push_back(1);}

if(_elems.size()<=pos)

{

int ix=_elems.size();

int n_2=_elems[ix-2];

int n_1=_elems[ix-1];

for(;ix<=pos;++ix)

{

int elem=n_2+n_1;

_elems.push_back(elem);

n_2=n_1;

n_1=elem;

}

}

}

ostream& fibonacci::print(ostream&os)const

{

int elem_pos=_beg_pos-1;

int end_pos=elem_pos+_length;

if(end_pos>_elems.size())//当索引大于当前时,则生成元素。

fibonacci::gen_elems(end_pos);

while(elem_pos<end_pos)

os<<_elems[elem_pos++]<<' ';

return os;

}

注意,elem()和print()都会检查_elems()存有的元素是否足够,并且在元素个数不足时调用gen_elems()加以补足。我们该如何修改check_integrity()以使这个检验操作的确能检验出pos的有效性呢?一个可能的方式是,为fibonacci class编写一份check_integrity():

class fibonacci:public num_sequence{

public:

//....

protected:

bool check_integrity(int pos)const;

//...

}
于是,在fibonacci class内,对check_integrity()的每次都会被解析为派生类中的那一份函数实例。以elem()为例,对check_integrity()的调用的乃是fibonacci中的那一份函数实例。

int fibonacci::elem(int pos)const

{

//现在调用的是fibonacci的check_integrity()

if(!check_integrity(pos))

return 0;

//....

}

每当派生类有某个member与其基类的同名,便会遮掩住基类的那份member。也就是说,派生类对该名称的任何使用,都会被解析为该派生类自身的那份member,而非继承而来的那份。这种情况下,如果要在派生类中使用继承来的那份member,必须利用运算符加以限定,例如:

 

inline bool fibonacci::check_integrity(int pos)const

{

//必须加上class scope运算符

//如果未经限定,会解析为派生类自身的函数!

if(!num_sequence::check_integrity(pos))

return false;

if(pos>_elems.size())

fibonacci::gen_elems(pos);

return true;

}

这种解决方法带来的问题是,在基类中check_integritu()并未被视为虚函数,于是,每次通过基类的pointer或reference来调用check_integrity(),解析出来的都是num_sequence的那一份,未考虑到pointer或reference实际指向的究竟是什么对象。例如:

void fibonacci::example()

{

num_sequence *ps=new fibonacci(12,8);

//ok:通过虚拟机制动态解析Wiefibonacci::elem()

ps->elem(1024);

//根据ps的类型,以下就会被静态解析为

//num_sequence::check_integrity()

ps->check_integrity(pos);

}

 

基于这个原因,一般而言,在基类和派生类中提供同名的non-virtual函数,并不是好的解决办法。基于此点而归纳出来的结论或许是:基类中的所有函数都应该被声明为virtual。我不认为这是一个正确的结论,但它的确可以马上解决我们所面临的两难困境。
 

造成这两种困境的深层原因是,当派生类欲检查其自身状态的完整性时,已实现完成的基类缺少足够的知识。而我们知道,根据不完整的信息所完成的实现,可能也是不完整的。这和“宣称实现和类型相关,因而必须将它声明为virtual”的情形并不相同。


我再重复一次,我认为,所谓设计,必须来来回回的借着程序眼的经验和用户的反馈演进。本例中,较好的解决方案是重新定义check_integrity(),另它有两个参数:

bool num_sequence::check_integrity(int pos,int size)

{

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

{

//和先前相同

}

if(pos>size)

//gen_elems()通过虚拟调用

gen_elems(pos);

return true;

}


在这个check_integrity()版本中,gen_elems()通过虚拟机制被调用,如果check_integrity()是由fibonacci对象调用,那么后续就会调用fibonacci的gen_elems(),如果check_integrity()是由triangular对象调用,那么后续就会调用triangular的gen_elems(),以此类推。新的版本会以以下的方式被使用:

 

int fibonacci::elem(int pos)

{

if(!check_integrity(pos,elems.size()))

return 0;

//...

}
逐步测试自己的实现代码,比整个程序都完成后才测试好多了。这不仅可以让我们更稳健的检查我们所完成的内容,更可以提供完整的一套回归测试的基础,使我们得以继续演进并改进原本的设计。以下便是一个小型的测试程序,用来操练截止目前完成的实现代码。中临时加上了显示功能,可显示它所产生的元素。

int main()

{

fibonacci fib;

cout<<"fib:beginning at element 1 for 1 element:"

<<fib<<endl;

 

fibonacci fib2(16);

cout<<"fib:beginning at element 1 for 116elements:"

<<fib2<<endl;

 

fibonacci fib2(8,12);

cout<<"fib:beginning at element 12 for 8elements:"

<<fib3<<endl;

 

}

编译并执行,将产生以下的输出结果:

fib:beginning at element 1 for 1 element:(1,1),1

 

fib2:beginning at element 1 for 16 elemenst:

gen_elems:2

gen_elems:3

gen_elems:5

gen_elems:8

gen_elems:13

gen_elems:21

gen_elems:34

gen_elems:55

gen_elems:89

gen_elems:144

gen_elems:233

gen_elems:377

gen_elems:610

gen_elems:987

(1,16)1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

 

fib3:beginning at element 12 for 18elements:

gen_elems:1597

gen_elems:2584

gen_elems:4181

(12,8)144 233 377 610 987 1597 2584 4181
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值