zoukankan      html  css  js  c++  java
  • C++进阶之虚函数表

    C++通过继承(inheritance)虚函数(virtual function)来实现多态性。所谓多态,简单地说就是,将基类的指针或引用绑定到子类的实例,然后通过基类的指针或引用调用实际子类的成员函数(虚函数)。本文将介绍单继承、多重继承下虚函数的实现机制。

    一、虚函数表

    为了支持虚函数机制,编译器为每一个拥有虚函数的类的实例创建了一个虚函数表(virtual table),这个表中有许多的槽(slot),每个槽中存放的是虚函数的地址。虚函数表解决了继承、覆盖、添加虚函数的问题,保证其真实反应实际的函数。

    为了能够找到 virtual table,编译器在每个拥有虚函数的类的实例中插入了一个成员指针 vptr,指向虚函数表。下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Base
    {
    public:
    virtual void x() { cout << "Base::x()" << endl; }
    virtual void y() { cout << "Base::y()" << endl; }
    virtual void z() { cout << "Base::z()" << endl; }
    };

    typedef void(*pFun)(void);

    int main()
    {
    Base b;
    int* vptr = (int*)&b; // 虚函数表地址

    pFun func1 = (pFun)*((int*)*vptr); // 第一个函数
    pFun func2 = (pFun)*((int*)*vptr+1); // 第二个函数
    pFun func3 = (pFun)*((int*)*vptr+2); // 第三个函数

    func1(); // 输出Base::x()
    func2(); // 输出Base::y()
    func3(); // 输出Base::z()
    return 0;
    }

    上面定义了一个Base类,其中有三个虚函数。我们将Base类对象取址 &b 并强制转换为 int,取得虚函数表的地址。然后对虚函数表的地址取值 vptr 并强转为 int*,即取得第一个虚函数的地址了。将第一个虚函数的地址加1,取得第二个虚函数的地址,再加1即取得第三个虚函数的地址。

    注意,之所以可以通过对象实例的地址得到虚函数表,是因为 vptr 指针位于对象实例的最前面(这是由编译器决定的,主要是为了保证取到虚函数表有最高的性能——如果有多层继承或是多重继承的情况下)。如图所示:

    在VS2012中加断点进行Debug可以查看到虚函数表:

    二、单继承时的虚函数表

    1、无虚函数覆盖

    假如现有单继承关系如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Base
    {
    public:
    virtual void x() { cout << "Base::x()" << endl; }
    virtual void y() { cout << "Base::y()" << endl; }
    virtual void z() { cout << "Base::z()" << endl; }
    };

    class Derive : public Base
    {
    public:
    virtual void x1() { cout << "Derive::x1()" << endl; }
    virtual void y1() { cout << "Derive::y1()" << endl; }
    virtual void z1() { cout << "Derive::z1()" << endl; }
    };

    在这个单继承的关系中,子类没有重写父类的任何方法,而是加入了三个新的虚函数。Derive类实例的虚函数表布局如图示:

    • Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 实例的虚函数表对应的 slot 之中。
    • 新增的 虚函数 置于虚函数表的后面,并按声明顺序存放。

    2、有虚函数覆盖

    如果在继承关系中,子类重写了父类的虚函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Base
    {
    public:
    virtual void x() { cout << "Base::x()" << endl; }
    virtual void y() { cout << "Base::y()" << endl; }
    virtual void z() { cout << "Base::z()" << endl; }
    };

    class Derive : public Base
    {
    public:
    virtual void x() { cout << "Derive::x()" << endl; } // 重写
    virtual void y1() { cout << "Derive::y1()" << endl; }
    virtual void z1() { cout << "Derive::z1()" << endl; }
    };

    则Derive类实例的虚函数表布局为:

    相比于无覆盖的情况,只是把 Derive::x() 覆盖了Base::x(),即第一个槽的函数地址发生了变化,其他的没有变化。

    这时,如果通过绑定了子类对象的基类指针调用函数 x(),会执行 Derive 版本的 x(),这就是多态。

    三、多重继承时的虚函数表

    1、无虚函数覆盖

    现有如下的多重继承关系,子类没有覆盖父类的虚函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Base1
    {
    public:
    virtual void x() { cout << "Base1::x()" << endl; }
    virtual void y() { cout << "Base1::y()" << endl; }
    virtual void z() { cout << "Base1::z()" << endl; }
    };

    class Base2
    {
    public:
    virtual void x() { cout << "Base2::x()" << endl; }
    virtual void y() { cout << "Base2::y()" << endl; }
    virtual void z() { cout << "Base2::z()" << endl; }
    };

    class Derive : public Base1, public Base2
    {
    public:
    virtual void x1() { cout << "Derive::x1()" << endl; }
    virtual void y1() { cout << "Derive::y1()" << endl; }
    };

    对于 Derive 实例 d 的虚函数表布局,如下图:

    可以看出:

    • 每个基类子对象对应一个虚函数表。
    • 派生类中新增的虚函数放到第一个虚函数表的后面。

    测试代码(VS2012):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    typedef void(*pFun)(void);

    int main()
    {
    Derive b;
    int** vptr = (int**)&b; // 虚函数表地址

    // virtual table 1
    pFun table1_func1 = (pFun)*((int*)*vptr+0); // vptr[0][0]
    pFun table1_func2 = (pFun)*((int*)*vptr+1); // vptr[0][1]
    pFun table1_func3 = (pFun)*((int*)*vptr+2); // vptr[0][2]
    pFun table1_func4 = (pFun)*((int*)*vptr+3); // vptr[0][3]
    pFun table1_func5 = (pFun)*((int*)*vptr+4); // vptr[0][4]

    // virtual table 2
    pFun table2_func1 = (pFun)*((int*)*(vptr+1)+0); // vptr[1][0]
    pFun table2_func2 = (pFun)*((int*)*(vptr+1)+1); // vptr[1][1]
    pFun table2_func3 = (pFun)*((int*)*(vptr+1)+2); // vptr[1][2]

    // call
    table1_func1();
    table1_func2();
    table1_func3();
    table1_func4();
    table1_func5();

    table2_func1();
    table2_func2();
    table2_func3();
    return 0;
    }

    不同的编译器对 virtual table 的实现不同,经测试,在 g++ 中需要这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // virtual table 1
    pFun table1_func1 = (pFun)*((int*)*vptr+0); // vptr[0][0]
    pFun table1_func2 = (pFun)*((int*)*vptr+2); // vptr[0][2]
    pFun table1_func3 = (pFun)*((int*)*vptr+4); // vptr[0][4]
    pFun table1_func4 = (pFun)*((int*)*vptr+6); // vptr[0][6]
    pFun table1_func5 = (pFun)*((int*)*vptr+8); // vptr[0][8]

    // virtual table 2
    pFun table2_func1 = (pFun)*((int*)*(vptr+1)+0); // vptr[1][0]
    pFun table2_func2 = (pFun)*((int*)*(vptr+1)+2); // vptr[1][2]
    pFun table2_func3 = (pFun)*((int*)*(vptr+1)+4); // vptr[1][4]

    2、有虚函数覆盖

    将上面的多重继承关系稍作修改,让子类重写基类的 x() 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Base1
    {
    public:
    virtual void x() { cout << "Base1::x()" << endl; }
    virtual void y() { cout << "Base1::y()" << endl; }
    virtual void z() { cout << "Base1::z()" << endl; }
    };

    class Base2
    {
    public:
    virtual void x() { cout << "Base2::x()" << endl; }
    virtual void y() { cout << "Base2::y()" << endl; }
    virtual void z() { cout << "Base2::z()" << endl; }
    };

    class Derive : public Base1, public Base2
    {
    public:
    virtual void x() { cout << "Derive::x()" << endl; } // 重写
    virtual void y1() { cout << "Derive::y1()" << endl; }
    };

    这时 Derive 实例的虚函数表布局会变成下面这个样子:

    相比于无覆盖的情况,只是将Derive::x()覆盖了Base1::x()Base2::x()而已,你可以自己写测试代码测试一下,这里就不再赘述了。

    注:若虚函数是 private 或 protected 的,我们照样可以通过访问虚函数表来访问这些虚函数,即上面的测试代码一样能运行。





    附:编译器对指针的调整

    在多重继承下,我们可以将子类实例绑定到任一父类的指针(或引用)上。以上述有覆盖的多重继承关系为例:

    1
    2
    3
    Derive b;
    Base1* ptr1 = &b; // 指向 b 的初始地址
    Base2* ptr2 = &b; // 指向 b 的第二个子对象
    • 因为 Base1 是第一个基类,所以 ptr1 指向的是 Derive 对象的起始地址,不需要调整指针(偏移)。
    • 因为 Base2 是第二个基类,所以必须对指针进行调整,即加上一个 offset,让 ptr2 指向 Base2 子对象。
    • 当然,上述过程是由编译器完成的。

    当然,你可以在VS2012里通过Debug看出 ptr1 和 ptr2 是不同的,我们可以这样子:

    1
    2
    3
    4
    Base1* b1 = (Base1*)ptr2;  
    b1->y(); // 输出 Base2::y()
    Base2* b2 = (Base2*)ptr1;
    b2->y(); // 输出 Base1::y()

    其实,通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。由于 ptr2 本来就指向 Base2 子对象的起始地址,所以b1->y()调用到的是Base2::y(),而 ptr1 本来就指向 Base1 子对象的起始地址(即 Derive对象的起始地址),所以b2->y()调用到的是Base1::y()

    https://www.cnblogs.com/aademeng/articles/7265036.html

  • 相关阅读:
    一段自己写的丑陋的表单验证代码
    简单的星级评价
    有个项目
    好久没写了,重装了系统重新配置的Live Writer,看看效果:
    XmlHttpRequest调用Webservice的一点心得
    局域网共享怎么设置?我想把其中一个电脑的F盘共享?
    TCP/IP协议详解
    input file实现多张图片上传
    .NET C#中如何备份SQL数据库
    CSS中cursor鼠标形状属性列表
  • 原文地址:https://www.cnblogs.com/findumars/p/9845429.html
Copyright © 2011-2022 走看看