C++类的继承概念辨析:虚函数,虚函数表,抽象基类,纯虚函数,虚基类,虚继承

虚函数和虚函数表

在了解什么是虚函数之前,首先要理解什么是动态绑定。

动态绑定
  • 动态绑定是C++类指针或引用的特性,当编译器遇到一个基类指针或引用时,并不直接确定其类型,而是在运行时根据其具体指向来调用对应的函数。
  • 为什么基类指针和引用可以指向派生类呢?因为类的继承关系是一种is a的关系,即派生类是特殊的基类,因此基类指针和引用可以指向派生类,但是此时的基类指针和引用,只能访问基类的对象和成员函数。如果想要在不发生强制类型转换的情况下访问派生类的成员函数,就要用到虚函数的概念。
虚函数应用示例
  • 虚函数的实现前提是,派生类和基类中存在同样的函数,名字和形参列表都一致,在基类中该函数有virtual关键字声明,派生类中无所谓。
  • 于是当程序执行遇到基类指针或者引用时,先判断指向的类型,再根据类型调用相关的函数。注意:只要是基类指针指向的虚函数调用,均会实行动态绑定,容易忽略的一点是在基类的成员函数内部发生的调用,隐含了this指针,所以同样会发生动态绑定。可以观察以下代码,猜测其输出,然后和运行结果进行对比。
#include <iostream>

using namespace std;
class base
{
public:
    virtual void display()
    {
        cout<<"I am  base display\n";
    }
    void display(int i)
    {
        cout<<"base i="<<i<<endl;
    }
    void show()
    {
        cout<<"this is base show"<<endl;
        display();
    }
};
class derived:public base
{
public:
    void display( )
    {
        cout<<"I am derived display\n";
    }
    void display(int i)
    {
        cout <<"derived i ="<<i<<endl;
    }
    void show()
    {
        cout<<"this is derived show"<<endl;
        display();
    }
};
int main()
{
    base *b;
    derived d;
    b=&d;
    b->base::display();
    b->display(10);
    b->show();
    return 0;
}
/*运行结果:
I am  base display
base i=10
this is base show
I am derived display
*/
  • 在上述例子中同时涉及了函数重载和虚函数的动态绑定。可以看到即使是在基类的非虚函数中调用虚函数,也会发生动态绑定。
  • 想要直接,确定的调用基类的成员函数,必须使用类限定符。而如果不使用虚函数,使用派生类限定符直接调用派生类的成员函数,此时会进行静态绑定,编译器检测到调用与指针类型不符合,会报错。
虚函数的实现:虚函数表

虚函数具体是如何实现的呢?如果一个成员函数是虚函数,那么在基类指针发生虚函数调用的地方,编译器不会给出函数调用地址,而是检索基类的派生链,找到其所有派生类,建立一个虚函数表,表的内容是对象类型,和对应的虚函数的调用地址。在程序运行时,根据对象类型找到对应的虚函数入口,从而实现了动态绑定。

虚函数的意义

虚函数有什么作用呢?虚函数使得我们在编写程序时不需要考虑对象的具体类型,可以使用统一的代码来完成对不同类型的对象的处理。相当于对上层函数隐藏了各种复制派生类的具体实现,而提供了统一的接口,就像交通工具类可以派生出轮船,飞机,汽车火车,每个派生类都有move函数,move的方式也不一样,但是需要调用交通工具类进行move操作的函数则不需要关心被调用的对象是如何实现move的,它只需要使用一个交通工具类对象调用move函数就可以了。

抽象类和纯虚函数

在虚函数的基础上,进一步设想,假如我现在要设计一个基类来派生出不同的子类,我要求每种子类都要实现某个功能。但是基类不需要或者没有办法实现这个功能,只是提供了一个大致框架方便统一调用,我该怎么办呢?

  • 学过java的同学会想到java中的接口类型,只需给出函数的声明而不需要实现。在C++中类似的功能由抽象类实现。
  • 就像上述提到的交通工具类派生出轮船,飞机,汽车、火车类,每个类都要有move函数,所以move函数应该是虚函数才能实现动态绑定。但是没有具体指明交通工具类型时,我们无法对move函数进行定义,所以此时可以把交通工具类声明为抽象类。
  • 抽象类就是其中的虚函数只有声明,没有定义的类型,我们用虚函数声明=0的形式来进行表示。这种虚函数,称为纯虚函数。
  • 抽象类,顾名思义,抽象的,没有具体实现,因此无法生成其对象,而抽象类的派生类必须对抽象类中的纯虚函数进行实现,否则仍然是抽象类。

虚基类和虚继承

虚基类和虚继承主要是用来解决交叉继承的问题。

  • 在普通继承方式中,派生类继承基类的同时,派生类的对象会生成一份基类对象,如果- 基类对象又有基类,派生类同时还会拥有一个间接基类对象。问题来了,派生类可能会拥有两个或者多个直接基类,如果这些直接基类又是从同一个基类继承而来,那么派生类就会拥有许多个间接基类的对象。
  • 而在有些情况下,我们希望所有的间接基类继承都只会指向同一个间接基类对象,在这种情况下就要用到虚继承,而虚继承对应的间接基类,也就是那个我们只需要一份对象的间接基类,称为虚基类。
  • 所以事实上,虚基类不是类本身的特性,而是在某个类被虚继承之后,我们给他的称呼。任何一个非抽象类,都可以用来声明虚继承,都可以是一个虚基类。
  • 同时,由于虚基类必须保证其对象的唯一性,因此虚基类的对象实例化必须由最底层的派生类显式给出,否则就会产生冲突。也即是说,如果一个类声明了虚继承,则虚基类对象的构造需要也必须由这个类的派生类实现,除非这个类没有派生类。
虚基类的代码实现
#include <iostream>
using namespace std;

class transportation
{
public:
    transportation(int i)
    {
        cout<<"transportation "<<i<<" born"<<endl;
    }
};
class car:virtual public transportation
{
public:
    car(int i):transportation(i)
    {
        cout<<"car "<<i<<" born"<<endl;
    }
};
class plane:virtual public transportation
{
public:
    plane(int i):transportation(i)
    {
        cout<<"plane "<<i<<" born"<<endl;
    }
};
class carplane:public car,public plane
{
public:
    carplane(int i ):car(i+2),plane(i+3)
    {
        cout<<"carplane "<<i<<" born"<<endl;
    }
};
int main()
{
    carplane t(0);
    return 0;
}

/*程序输出
transportation 1 born
car 2 born
plane 3 born
carplane 0 born
*/
/*如果没有使用虚继承的方式,则程序输入为:
transportation 2 born
car 2 born
transportation 3 born
plane 3 born
carplane 0 born
*/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章