首页 >> 大全

虚函数与纯虚函数(收录学习)

2023-07-28 大全 26 作者:考证青年

收录:

我们把一个仅仅含有纯虚函数的类称为接口,我们也好像已经习惯了将这个接口中的所有纯虚函数全声明为,而且按照这样的设计,一切都工作得不错。比如COM正是这样做的,它的接口中几乎不会存在的纯虚函数。那么,让我们想一想,纯虚函数或者虚函数可以为吗?如果这种方式是可行的,那么什么时候可以将(纯)虚函数设为了?这些都是本文将要讨论的主题。一起来看看。

一.访问限定符与继承

如果基类隐式(间接)向子类暴露了私有成员,那么从某种意义上讲,该私有成员对于子类是可见的。

任何一本讲C++基础的课本上都详细地介绍了访问限定符与继承的关系,在这里就不重复了,但是,课本上的东西并不全,不信?那么请先看看下面的例子:

怎么回事?为什么不是

this class id is

this is !

子类的()不是将基类的()覆盖了么?我们来分析一下,基类中的公共的work()成员函数调用了私有的()成员函数,根据输出的结果来看,在子类中定义的方法并没有覆盖基类的同名方法,为什么呢?难道是因为是导致的?那好,我们将函数改为再次运行,我们期望的结果出现了吗?呵呵,很抱歉,没有,希望再次破灭了,为什么会这样?这主要涉及的原因是:普通函数的调用是在编译期确定的,当work函数一看到所调用的是非虚的,就会毫无疑问地去直接使用基类的。这一切与Base类是否会被继承没有任何关系,跟Base类被继承后子类会否再次定义就更没有关系了。

那么这种情况下,Base类将声明为和/有什么区别了?当将声明为时,看不到基类的的声明,所以不会发生重定义;当将声明为/时,将看到基类的声明,于是会发生重定义,即会覆盖调基类的的定义。讲到这里就要提一下,如果当将声明为/,并且子类也定义同名的函数,但是子类的与基类的的函数签名不同,那么此时发生的将是函数重载而不是覆盖。

让我们更进一步,将基类和子类的声明都改为 ,再次运行程序,会得到以下输出:

this class id is

this is !

而这正是我们所期望的,不是吗?这其中的原因也很容易理解,因为是 ,并且是的,所以会产生多态调用。

再往下走,将基类和子类的声明改为 ,再次运行程序,看看输出了什么。

this class id is

this is !

没有变化,将声明为 和声明为 得到的结果是一样的。“为什么会这样,是啊?”你惊讶地叫出来。是,是,但也是,原因就在这里,用基类指针或引用进行虚函数调用采用的是动态绑定,看看编译器为调用产生的代码就知道了:

//c++伪码

(this->vptr[1])() ;

在运行时期,通过this指针将会找到正确的vtbl,即类的vtbl,这样自然就会出现上面的结果了。那么将 声明为限制了什么?和将非虚函数声明为一样,这将使得在Base类外部无法调用多态函数,只能在Base内部调用,如通过work函数调用。

可见,多态性与将实现多态的函数的访问限定符没有任何关系, 函数仍然可以实现多态,它的指针仍然位于vtbl中,只不过该函数的多态一般只能在基类的内部由其他非虚函数调用该函数的时候反映出来,访问限定符仅仅限制外部对类的成员的访问权限,它并没有破坏以下规则:

通过基类指针或引用调用成员函数时,如果该函数时非虚的,那么将采用静态绑定,即编译时绑定;如果该函数是虚拟的,则采用动态绑定,即运行时绑定。

二. 与访问限定符结合

_虚函数的实现原理_虚函数表详解

上面我们通过分析,已经知道了多态的实现与访问限定符没有任何关系,访问限定符只是控制类的成员对外部的可见性,但不限制多态。正如上面提到的,将声明为 和声明为 后再次运行程序,得到的结果是一样的,上面我们简单的地分析了一下表面现象,但这个问题决不是这么简单,让我们挖掘更深层次的意义,我想这应该属于OOA、OOD的范畴了。好,让我们一步步看过来。

当我们将声明为非虚的 时,子类将看不见它,当然也就无法覆盖或重载它,即在这中情况下,子类无法更改的实现,但是子类继承了公共接口work(),而这个接口调用了,所以,可以看作,子类间接地继承了的实现,并且这个实现是无法修改的。于是,我可以说,基类中声明一个普通私有成员函数,表示这是一个不可被更改的实现细节。

再来讨论将声明为 的情况,声明为表示基类不想让子类看到这个函数,但是又声明为,表示基类想让这个函数实现多态。呵呵,基类既想实现多态,却又不让子类看见这个函数,这似乎有点自相矛盾,是吗?其实,这其中的意思是,子类既可以修改这个实现,也可以继承其基类默认的实现。所以可以这么说,如果基类中有一个虚拟私有成员函数,表示这是一个“可以”被派生类修改的实现细节。注意,当中的用词,是“可以”,而不是别的。

最后来看看将声明为 的情况。将声明为表示基类“需要”子类看见这个函数,注意,我使用“需要”这个动词,这个词表示了一定的“强制”意味。与将声明为 的情况对比一下,我想你已经知道答案了,即是,如果基类中有一个虚拟保护成员函数,表示这是一个必须被派生类修改的实现细节。“必须”这个词表达了强制的意思。

关于“将与访问限定符结合”的问题就讨论这么多,你也许说,还漏掉了将声明为 的情况。是的,其实,我并不推荐将虚拟函数声明为,尽管这种方式在现在很流行,我推荐将其使用 来替换,这就说明基类必须另外发布一个几乎不更改的非虚接口,在这个接口中调用了 或 函数,这样以来,我们就对类的内部实现作了进一步的隐藏,而这无论是对系统的可扩展性,还是可维护性都是大有帮助的。“虚拟函数应该和数据成员一样对待――让他们成为私有的,除非设计需求表明应该有较少的限制。提升它们到更高存取级别比把它们降到更私有的级别更容易些。”

最后,把上面所说的小结一下:

基类中的一个普通私有成员函数,表示这是一个不可被更改的实现细节。

基类中的一个虚拟私有成员函数,表示这是一个可以被派生类修改的实现细节。

基类中的一个虚拟保护成员函数,表示这是一个必须被派生类修改的实现细节。

最好不要将虚拟成员函数声明为,而是用来替换。

三.模板方法模式

在理解了上面所述的内容的情况下,再来理解模板方法模式就非常easy了,模板方法是在GOF的经典大作《设计模式》中阐述了一种模式,该模式定义了一个操作中的算法的骨架,而将一些步骤的实现延迟到子类中,模板方法使得派生类可以不改变一个算法的结构即可重定义算法的某些特定步骤。在这里,我不想再重复解释这个模式如何实现的,我仅仅举个例子,这个例子将体现出模板方法中最重要的思想。

假设基类定义的一个算法的骨架由3个步骤完成,其中第一个步骤是该继承体系中不可被改变的一个步骤,即所有的类对该步骤的实现都是一样的,那个这个步骤可以设置为非虚的 ;第二个步骤是一个可以被派生类改写也可以不被改写的步骤,通过上面的讨论知道,可以将其设为 ;第三个步骤是针对每一个派生类的实现都不同,那么这个步骤可以被设为 ,而且,步骤三只能针对特定的派生类才有意义,所以将步骤三也设为纯虚函数。如下面的代码所示:

work()这个非虚的接口展现出来的,当我们用一个指针调用work()时,表面上是一个非虚函数调用,采用静态绑定,事实上也正是这样,但是,这个调用的背后隐藏的却是多态调用,即step2和step3动态绑定了。看见,采用模板方法模式,不仅定义了一个算法的骨架,而且把这个骨架的实现的细节作了进一步的封装。我们可以在模板方法模式中可以这样设计:

class

:

(void)//不可被更改的实现细节

(void)//可以被派生类修改的实现细节

:

(void)=0;//必须被派生类修改的实现细节

:

(void)//骨架函数,实现了骨架

step1();

step2();

step3();

};

注意,上例中根本没有暴露任何虚函数,所有的这一切都是通过

(1) 如果一个函数作为算法骨架中不可变更的一部分,那么可以将此函数作为基类的私有函数,并且在基类的公共骨架函数中调用该函数,即该函数作为骨架的一个不可更改的实现细节。

(2) 如果一个函数提供了算法骨架某环节的一个缺省实现,那么可以考虑将该函数作为基类的私有虚函数,表示子类可以改写它,也可以不改写它。

(3) 如果作为算法骨架一部分某个函数要求在子类中拥有不同的实现,那么可以考虑将该函数作为基类的保护(纯)虚函数,表示子类必须改写它。

讲到这里,已经差不多了,在结束的时候,提一下语法与语义的联系。通常,语法是表象,语义是表象后面隐藏的东西,而这些隐藏的语义往往更具有价值。举个例子,继承与继承在语法方面似乎没有什么更多的东西值得探讨,它们的区别仅仅在于改变了继承得到的成员的可见性,但是从语义方面来分析,它们就相差太远了,继承在语义上来讲是“通过基类来实现自己”,即是“实现继承”,在这种继承关系中,基类和子类的关系是很薄弱的;而继承在语义上即是我们所熟知的“IS-A”关系,它体现了基类和子类之间的亲密性,也正是这种“IS-A”关系为多态性提供了基础。

所以,通过表面的语法来挖掘其背后的语义很有意义,就像这篇文章中提到的将访问限定符与结合起来的语法背后隐藏的语义,挖掘出这些语义,对于我们以后在进行设计时作恰当的抉择无疑是大有帮助的。

#

#

std;

class Base

:

()const

("Base" );

:

()=0;//纯虚函数

:

void work()

cout

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了