美文网首页C++工作需要的技能c/c++
C++ 虚函数表、虚函数讲解

C++ 虚函数表、虚函数讲解

作者: frederickhou | 来源:发表于2019-08-09 17:20 被阅读0次

    前言

    近期有不少同学私信我询问关于C++ 虚表和虚函数的相关问题,于是就打算写一篇关于C++虚函数和虚表的原理文章有助于大家更好的去理解和学习。


    虚函数

    概念

    虚函数是一种在基类中用virtual关键字声明的函数,并在一个或多个派生类中再定义的函数。虚函数的特点是,只要定义一个基类的指针,就可以指向派生类的对象。

    [注:无虚函数时,遵循以下规则:C++规定,定义为基类的指针,也能作指向派生类的指针使用,并可以用这个指向派生类对象的指针访问继承来的基类成员;但不能用它访问派生类的成员。]

    • 使用虚函数实现运行时的多态性的关键在于:必须通过基类指针访问这些函数。

    • 一旦一个函数定义为虚函数,无论它传下去多少层,一直保持为虚函数。

    • 把虚函数的再定义称为过载(overriding)而不叫重载(overloading)。

    • 纯虚函数:是定义在基类中的一种只给出函数原型,而没有任何与该基类有关的定义的函数。纯虚函数使得任何派生类都必须定义自己的函数版本。否则编译报错。纯虚函数定义的一般形式:

      virtual type func_name(args)=0;
    
    • 含有纯虚函数的基类称为抽象基类。抽象基类又一个重要特性:抽象类不能建立对象。但是抽象基类可以有指向自己的指针,以支持运行时的多态性。

    虚函数示例代码

    #include"test.h"
    #include<iostream>
    using namespace std;
    
    class Base{
    
    public:
    
        void printf()
        {
            cout << "Base printf()" << endl;
        }
    
        virtual void func()
        {
            cout << "Base func()" << endl;
        }
    
    };
    
    class Derived:public Base{
    
    public:
    
        void printf()
        {
            cout << "Derived printf()" << endl;
        }
    
        virtual void func()
        {
            cout << "Derived func()" << endl;
        }
    
    };
    

    示例讲解

    在以上示例代码中,我们声明了一个父类 Base,和它的一个派生类 Derive,其中 printf() 实例方法是非虚函数,而func()方法被声明为了虚函数。并且在子类中我们重新实现了printf() 和 func()方法。下面我们分别构造出一个 Derive 实例和Base 实例,分别用示例对象访问各func()和printf()方法。然后构造新的Derived实例,并分别将其地址赋给 Base 指针和 Derived 指针,然后分别输出访问func()和printf()方法的结果:

    int main()
    {
        Base baseObj = Base();
        baseObj.func();
        baseObj.printf();
        Derived derivedObj = Derived();
        derivedObj.func();
        derivedObj.printf();
    
        Derived* pDerivedObj = new Derived();
        Base* pBaseObj = pDerivedObj;
        pDerivedObj->func();
        pBaseObj->func();
        pDerivedObj->printf();
        pBaseObj->printf();
        delete pDerivedObj;
        return 0;
    }
    

    运行结果

    Terminal output result:
    
    Base func()
    Base printf()
    Derived func()
    Derived printf()
    
    Derived func()
    Derived func()
    Derived printf()
    Base printf()
    

    结果描述

    Base和Derived实例分别访问func()和printf()方法。运行结果为各自对应的func()和printf()方法输出。
    pDerivedObj 和 pBaseObj指针分别指向了Derived实例的地址,对于 pDerivedObj 指针的操作表现出来它本身的方法输出,然而当我们把相同对象的地址赋给 pBaseObj 指针时,可以发现它的非虚函数printf()竟然表现出了父类的行为,并没有被重写的样子。那到底是什么原因造成了这样的结果呢?我们继续往下看虚函数表的介绍。

    虚函数表以及内存布局

    虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

    这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

    示例代码(一下示例代码编译环境是X86并且采用4byte对齐)

    非虚函数类

    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
    };
    

    内存布局情况

      class Base1   size(8):
        +---
       0    | a
       4    | c
            | <alignment member> (size=3)
        +---
    +---
    

    博主未来为了让同学们注意一下在类内存布局中常见的字节对齐问题,就专门在Base1类中添加了char c变量。可以很清晰的看出在内存中a和c成员变量依据声明的顺序进行排列(类内偏移为0开始)并且有3字节用于对齐,成员函数不占内存空间。

    单继承派生类不含非虚函数

    class DerivedClass : public Base1
    {
        int c;
    public:
        void DerivedCommonFunction() {};
    };
    

    内存布局情况

      class DerivedClass    size(12):
        +---
       0    | +--- (base class Base1)
       0    | | a
       4    | | c
            | | <alignment member> (size=3)
        | +---
       8    | c
        +---
    

    可以看到子类DerivedClass继承了父类Base1的成员变量,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,同样,成员函数不占字节。

    存在虚函数类

    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    

    内存分布情况

      class Base1   size(12):
        +---
       0    | {vfptr}
       4    | a
       8    | c
            | <alignment member> (size=3)
        +---
    
      Base1::$vftable@:
        | &Base1_meta
        |  0
       0    | &Base1::VirtualFunction
    

    这个内存结构图分成了两个部分,上面是内存分布,下面是虚表,我们逐个看一下。从上图可以看出虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面生成了虚表,紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,因为博主只写了一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数列出来。

    通过上面这个例子有同学就问了虚表指针以及虚表是什么时候创建的呢? 构造函数创建的时候即类对象实例化的时候就创建的。那么如何利用虚表指针与虚表来实现多态的呢? 当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)从而可以实现多态。

    单继承派生类中也有虚函数并且存在覆盖继承

    class DerivedClass : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    

    内存分布情况

      class DerivedClass    size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | | <alignment member> (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass::$vftable@:
        | &DerivedClass_meta
        |  0
       0    | &DerivedClass::VirtualFunction
    

    上半部是内存分布,可以看到,虚表指针被继承了,且仍位于内存排布的起始处,下面是父类的成员变量a和c,最后是子类的成员变量d,注意虚表指针只有一个,子类并没有再生成虚表指针了;下半部的虚表情况与父类是一样的由于子类将父类的虚函数方法重写了即产生的虚表序号只有一个。

    单继承派生类中也有虚函数并且不存在覆盖继承

    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction1() {};
    };
    

    内存布局情况

      class DerivedClass    size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | | <alignment member> (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass::$vftable@:
        | &DerivedClass_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass::VirtualFunction1
    

    此种情况内存分布中上半部分也只有一个虚表指针变量内存分布依次排列,但是下方虚表的内容变化了,虚表的0号是父类的VirtualFunction,而1号放的是子类的VirtualFunction2。也就是说,如果定义了DerivedClass的对象,那么在构造时,虚表指针就会指向这个虚表,以后如果调用的是VirtualFunction,那么会从父类中寻找对应的虚函数,如果调用的是VirtualFunction1,那么会从子类中寻找对应的虚函数。

    单继承派生类中即存在覆盖虚函数也存在非覆盖虚函数继承

    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass : public Base1
    {
        int c;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
        void virtual VirtualFunction1() {};
    };
    

    内存布局情况

      class DerivedClass    size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | | <alignment member> (size=3)
        | +---
      12    | c
        +---
    
      DerivedClass::$vftable@:
        | &DerivedClass_meta
        |  0
       0    | &DerivedClass::VirtualFunction
       1    | &DerivedClass::VirtualFunction1
    

    根据上面的内存布局情况,我们既重写了父类的虚函数,也有新添的虚函数,最终虚函数表0号和1号都是子类对应的虚函数地址。

    多继承派生类中存在覆盖虚函数继承

    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass1 : public Base1
    {
        int b;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass2 : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
    {
        int e;
    public:
        void DerivedDerivedCommonFunction() {};
        void virtual VirtualFunction() {};
    };
    

    内存布局

    class Base1 size(12):
    +---
    0   | {vfptr}
    4   | a
    8   | c
        | <alignment member> (size=3)
    +---
    
    Base1::$vftable@:
    | &Base1_meta
    |  0
    0   | &Base1::VirtualFunction
    
    class DerivedClass1 size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | | <alignment member> (size=3)
        | +---
      12    | b
        +---
    
      DerivedClass1::$vftable@:
        | &DerivedClass1_meta
        |  0
       0    | &DerivedClass1::VirtualFunction
    
      DerivedClass1::VirtualFunction this adjustor: 0
    
      class DerivedClass2   size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | | <alignment member> (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass2::$vftable@:
        | &DerivedClass2_meta
        |  0
       0    | &DerivedClass2::VirtualFunction
    
      DerivedClass2::VirtualFunction this adjustor: 0
    
      class DerivedDerivedClass size(36):
        +---
       0    | +--- (base class DerivedClass1)
       0    | | +--- (base class Base1)
       0    | | | {vfptr}
       4    | | | a
       8    | | | c
            | | | <alignment member> (size=3)
        | | +---
      12    | | b
        | +---
      16    | +--- (base class DerivedClass2)
      16    | | +--- (base class Base1)
      16    | | | {vfptr}
      20    | | | a
      24    | | | c
            | | | <alignment member> (size=3)
        | | +---
      28    | | d
        | +---
      32    | e
        +---
    
      DerivedDerivedClass::$vftable@DerivedClass1@:
        | &DerivedDerivedClass_meta
        |  0
       0    | &DerivedDerivedClass::VirtualFunction
    
      DerivedDerivedClass::$vftable@DerivedClass2@:
        | -16
       0    | &thunk: this-=16; goto DerivedDerivedClass::VirtualFunction
    

    根据上面的内存分布情况,此多继承覆盖情况,我分别把每个类的内存分布都打了出来,下面我们重点看看这个类DerivedDerivedClass,由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与DerivedClass2,还有自身的成员变量e。DerivedClass1包含了它的成员变量b,以及Base1,Base1有一个0地址偏移的虚表指针,然后是成员变量a和c;DerivedClass2的内存排布类似于DerivedClass1,注意到DerivedClass2里面竟然也有一份Base1。
    我们再来看看虚表继承情况,我们看到了有两份虚表了,分别针对DerivedClass1与DerivedClass2,在&DerivedDericedClass_meta下方的数字是首地址偏移量0也是DerivedClass1中的{vfptr}虚函数指针在DerivedDerivedClass的内存偏移,靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移。

    DerivedDerivedClass()的虚表的VirtualFunction()指针
    [站外图片上传中...(image-a847ce-1565342394654)]

    多继承派生类中不存在覆盖虚函数继承

    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass1 : public Base1
    {
        int b;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction1() {};
    };
    
    class DerivedClass2 : public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction2() {};
    };
    
    class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
    {
        int e;
    public:
        void DerivedDerivedCommonFunction() {};
        void virtual VirtualFunction3() {};
    };
    
    

    内存布局情况

        class Base1 size(12):
        +---
        0   | {vfptr}
        4   | a
        8   | c
            | <alignment member> (size=3)
        +---
    
        Base1::$vftable@:
        | &Base1_meta
        |  0
        0   | &Base1::VirtualFunction
    
      class DerivedClass1   size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | | <alignment member> (size=3)
        | +---
      12    | b
        +---
    
      DerivedClass1::$vftable@:
        | &DerivedClass1_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass1::VirtualFunction1
    
      DerivedClass1::VirtualFunction1 this adjustor: 0
    
      class DerivedClass2   size(16):
        +---
       0    | +--- (base class Base1)
       0    | | {vfptr}
       4    | | a
       8    | | c
            | | <alignment member> (size=3)
        | +---
      12    | d
        +---
    
      DerivedClass2::$vftable@:
        | &DerivedClass2_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass2::VirtualFunction2
    
      DerivedClass2::VirtualFunction2 this adjustor: 0
    
      class DerivedDerivedClass size(36):
        +---
       0    | +--- (base class DerivedClass1)
       0    | | +--- (base class Base1)
       0    | | | {vfptr}
       4    | | | a
       8    | | | c
            | | | <alignment member> (size=3)
        | | +---
      12    | | b
        | +---
      16    | +--- (base class DerivedClass2)
      16    | | +--- (base class Base1)
      16    | | | {vfptr}
      20    | | | a
      24    | | | c
            | | | <alignment member> (size=3)
        | | +---
      28    | | d
        | +---
      32    | e
        +---
    
      DerivedDerivedClass::$vftable@DerivedClass1@:
        | &DerivedDerivedClass_meta
        |  0
       0    | &Base1::VirtualFunction
       1    | &DerivedClass1::VirtualFunction1
       2    | &DerivedDerivedClass::VirtualFunction3
    
      DerivedDerivedClass::$vftable@DerivedClass2@:
        | -16
       0    | &Base1::VirtualFunction
       1    | &DerivedClass2::VirtualFunction2
    

    此种情况的内存分布和覆盖多继承一样,唯一注意的就是在多继承中成员虚函数地址会保存到第一个继承父类的虚函数表。

    多继承之虚继承派生类中存在覆盖虚函数继承

    class Base1
    {
        int a;
        char c;
    public:
        void CommonFunction() {};
        void virtual VirtualFunction() {};
    };
    
    class DerivedClass1 : virtual public Base1
    {
        int b;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction1() {};
    };
    
    class DerivedClass2 : virtual public Base1
    {
        int d;
    public:
        void DerivedCommonFunction() {};
        void virtual VirtualFunction2() {};
    };
    
    class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
    {
        int e;
    public:
        void DerivedDerivedCommonFunction() {};
        void virtual VirtualFunction3() {};
    };
    

    内存分布情况

        class Base1 size(12):
        +---
        0   | {vfptr}
        4   | a
        8   | c
            | <alignment member> (size=3)
        +---
    
        Base1::$vftable@:
        | &Base1_meta
        |  0
        0   | &Base1::VirtualFunction
    
      class DerivedClass1   size(24):
        +---
       0    | {vfptr}
       4    | {vbptr}
       8    | b
        +---
        +--- (virtual base Base1)
      12    | {vfptr}
      16    | a
      20    | c
            | <alignment member> (size=3)
        +---
    
      DerivedClass1::$vftable@DerivedClass1@:
        | &DerivedClass1_meta
        |  0
       0    | &DerivedClass1::VirtualFunction1
    
      DerivedClass1::$vbtable@:
       0    | -4
       1    | 8 (DerivedClass1d(DerivedClass1+4)Base1)
    
      DerivedClass1::$vftable@Base1@:
        | -12
       0    | &Base1::VirtualFunction
    
      DerivedClass1::VirtualFunction1 this adjustor: 0
      vbi:     class  offset o.vbptr  o.vbte fVtorDisp
                 Base1      12       4       4 0
    
      class DerivedClass2   size(24):
        +---
       0    | {vfptr}
       4    | {vbptr}
       8    | d
        +---
        +--- (virtual base Base1)
      12    | {vfptr}
      16    | a
      20    | c
            | <alignment member> (size=3)
        +---
    
      DerivedClass2::$vftable@DerivedClass2@:
        | &DerivedClass2_meta
        |  0
       0    | &DerivedClass2::VirtualFunction2
    
      DerivedClass2::$vbtable@:
       0    | -4
       1    | 8 (DerivedClass2d(DerivedClass2+4)Base1)
    
      DerivedClass2::$vftable@Base1@:
        | -12
       0    | &Base1::VirtualFunction
    
      DerivedClass2::VirtualFunction2 this adjustor: 0
      vbi:     class  offset o.vbptr  o.vbte fVtorDisp
                 Base1      12       4       4 0
    
      class DerivedDerivedClass size(40):
        +---
       0    | +--- (base class DerivedClass1)
       0    | | {vfptr}
       4    | | {vbptr}
       8    | | b
        | +---
      12    | +--- (base class DerivedClass2)
      12    | | {vfptr}
      16    | | {vbptr}
      20    | | d
        | +---
      24    | e
        +---
        +--- (virtual base Base1)
      28    | {vfptr}
      32    | a
      36    | c
            | <alignment member> (size=3)
        +---
    
      DerivedDerivedClass::$vftable@DerivedClass1@:
        | &DerivedDerivedClass_meta
        |  0
       0    | &DerivedClass1::VirtualFunction1
       1    | &DerivedDerivedClass::VirtualFunction3
    
      DerivedDerivedClass::$vftable@DerivedClass2@:
        | -12
       0    | &DerivedClass2::VirtualFunction2
    
      DerivedDerivedClass::$vbtable@DerivedClass1@:
       0    | -4
       1    | 24 (DerivedDerivedClassd(DerivedClass1+4)Base1)
    
      DerivedDerivedClass::$vbtable@DerivedClass2@:
       0    | -4
       1    | 12 (DerivedDerivedClassd(DerivedClass2+4)Base1)
    
      DerivedDerivedClass::$vftable@Base1@:
        | -28
       0    | &Base1::VirtualFunction
    

    上面虚继承的内存分布不做过多的叙述,下来总结一下:
    虚继承的作用是减少了对基类的重复(在一般多继承中会造成二义性编译时出错,虚继承可以消除二义性),但是代价是增加了虚表指针的负担(更多的虚表指针)。根据以上示例当基类有虚函数时:

    • 1 每个类都有虚指针和虚表;

    • 2 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;

    • 3 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。

    博客著作权归本作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    相关文章

      网友评论

        本文标题:C++ 虚函数表、虚函数讲解

        本文链接:https://www.haomeiwen.com/subject/vtohjctx.html