谈谈virtual函数

来源:互联网 发布:java获取时间毫秒数 编辑:程序博客网 时间:2024/05/29 13:52

virtual , 写C++ 的都不会陌生吧,用于类的成员函数,用以表现对象多态的性质。

为多态基类声明virtual 函数

以前看书时,得到一条“黄金定律”(这是错误的):

永远应该以virtual 声明类的析构函数

如果不这么做,那么当类成为基类时,在回收对象内存,会发生不正确的行为,导致内存泄漏。这里就不在赘述细节了。

乍一看,很有道理不是么,防患于未然。

但是少年呀,不能这么年轻,轻易声明virtual 是要付出代价的

在C++中,virtual 的运行时判断行为是基于类的虚函数表实现的。虚函数表是要耗费内存的,而且也会产生效率问题。
所以,正确地使用virtual 是[1]:

polymorphic(带多态性质的)base class 应该声明一个virtual 析构函数。如果class 带有任何virtual 函数,它就应该拥有一个virtual 析构函数
Classes 的设计目的如果不是作为base classed 使用,或不是为了具备多态性,就不该声明virtual 析构函数

恰当的时候,声明virtual 析构函数是正确的。
现在考虑抽象类的声明。
抽象类定义为类中至少包含一个pure-virtual 函数(纯虚函数)。抽象类作为基类,也应该有一个virtual 析构函数。
于是,在实现上,我们可以取巧,干脆就声明一个pure-virtual 的析构函数好了:

class SomeClass{public:    virtual ~SomeClass() = 0;};SomeClass::~SomeClass() {} // 然而还是要提供定义

这是[1]中提到的一个技巧。

不在构造和析构过程中调用virtual 函数

假如,我们希望在类被构造的同时做一些操作,类似于log,或者更新 什么与该实例化对象相关的操作。
嗯,就让类在构造的时候,打印自己的类名好了。
等等,那如果类被继承了呢,而我又想把这个操作从构造函数中分离出来。
ok,仔细一想,于是就让那么函数成为virtual 好了。于是就有下如下的实现:

class Base{public:    Base() { print(); } //注意这里调用了virtual 函数    virtual ~Base(){}    virtual void print() = 0;};void Base::print(){ cout<<" Base "<<endl; }class Derived: public Base{public:    Derived(){}    void print() { cout<<" Derived "<<endl; }};

*这个实现是不正确的

那么,当我们实例化Derived class 的时候,会发生什么?

Derived d;

我们以为会输出

Derived

然而真正的行为却是

Base

为什么virtual 函数在这时候的行为表现得却不是virtual函数?
因为在base class构造期间,virtual 函数不是virtual 函数。[1]

答案就是那么直白,所以,对于上述的行为,print 会调用的版本才是Base 的版本,因为在执行到那一句的过程时,Derived class 的类型其实是Base class 。[1]

事实上,在编译的时候,会有warning 信息:

warning: pure virtual ‘virtual void Base::print()’ called from constructor
Base() { print(); } //注意这里调用了virtual 函数

解释也是很易懂:base 的构造函数早于derived 构造函数,当base 的构造函数执行的时候,derived 的成员变量处于未初始化阶段。如果virtual 函数下降至 derived class阶层,那么取得的成员变量是derived class 的(但它们却没有初始化),这会导致程序的不明确行为。

对应于析构函数,道理其实是一样的,这里不再解释了。

pure-virtual、impure-virtual、non-virtual函数与继承的含义

当类发生继承时,会继承基类的成员变量、成员函数。

对于成员函数的继承,又可以有pure-virtual、impure-virtual、non-virtual的区别。(在本文中,如果单指virtual 函数,那么表示的是impure-virtual)
而对于这些继承,含义区分如下[1]:

  • pure-virtual: 只继承接口
  • impure-virtual: 继承接口和缺省实现
  • non-virtual:继承接口和强制性实现继承

*这部分内容来自[1]条款35。

当virtual 函数拥有缺省参数时

有以下定义:

class Base{public:    virtual ~Base(){}    virtual void func(int i=3)    {        cout<<"Base : "<<i<<endl;    }};class Derived: public Base{public:    void func(int i)    {        cout<<" Derived : "<<i<<endl;    }};

如果有:

    Base b;    Derived d;    d.func();  //能够通过编译吗

答案是不行
当通过对象的静态绑定调用func 的时候,必须要指定参数值。

ok,那么现在有这样的调用:

    Base* pbd = &d;    pbd->func();  //ok

这是可行的,并且运行的是Derived 版本的函数。
也就是说,输出是:

Derived : 3

在[1] 中的解释如下:

若以对象调用此函数,一定要指定参数值。
因为静态绑定下,函数并不从其base继承缺省参数值。
但若以指针或引用调用此函数,可以不指定参数,因为动态绑定下,这个函数会从其base 继承缺省参数值。

很神奇不是吗,一个函数的调用结果是由其base 的缺省参数和derived 的实现一起贡献的。

现在,做一点改变,假如:

class Derived: public Base{public:    void func(int i=4)      //这里也指定了缺省参数    {        cout<<"Derived :"<<i<<endl;    }};

当有:

    Base* pbd = &d;    pbd->func();  

结果会是什么?
输出的是3,也就是继承自base 的缺省参数。

这样的演示是为了说明语法方面的问题。但从设计上来说,这并不合理。
多个不同的缺省参数,导致代码难以阅读,行为也不清晰。
那如果指定了一样的缺省参数呢?

class Derived: public Base{public:    void func(int i=3)      //这里指定和base 一样的缺省参数    {        cout<<"Derived :"<<i<<endl;    }};

也不合理,代码重复是个尴尬的问题,如果base中的缺省参数修改了,那么derived 中的缺省参数是不是也要一一修改。
其实,保持已经一开始的版本就好,但是在使用的时候,你必须心中有数,清楚地明白它的行为是怎么样的。

另外一种设计的替代方案是,采用NVI(non-virtual interface) 手法:令base class 内的一个public non-virtual 函数调用private virtual 函数,后者可被derived class 重新定义。

class Base{private:    virtual void doFunc(int i)    {        cout<<"Base : "<<i<<endl;    }public:    virtual ~Base(){}    void func(int i=3)    {        doFunc(i);    }};class Derived: public Base{private:    virtual void doFunc(int i)    {        cout<<"Derived :"<<i<<endl;    }public:};

最后,重申:
绝对不要重新定义一个继承而来的缺省参数值[1]

[参考资料]
[1] Scott Meyers 著, 侯捷译. Effective C++ 中文版: 改善程序技术与设计思维的 55 个有效做法[M]. 电子工业出版社, 2011.
(条款07:为多态基类声明virtual 析构函数;
条款09:绝不在构造和析构过程中调用virtual函数;
条款34:区分接口继承和实现继承;
条款37:绝对不要重新定义继承而来的缺省参数值)

0 0