c++虚函数实现机制及内存模型

2023-11-17

前言

大家都应该知道C++的精髓是虚函数吧? 虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的函数还是继承类的函数. 虚函数是实现多态(动态绑定)/接口函数的基础. 可以说: 没有虚函数, C++将变得一无是处!

既然是C++的精髓, 那么我们有必要了解一下她的实现方式吗? 有必要! 既然C++是从C语言的基础上发展而来的, 那么我们可以尝试用C语言来模拟实现吗? 有可能! 接下来, 就是我一步一步地来解析C++的虚函数的实现方式, 以及用C语言对其进行的模拟.

C++对象的内存布局

要想知道C++对象的内存布局, 可以有多种方式, 比如:

  1. 输出成员变量的偏移, 通过offsetof宏来得到
  2. 通过调试器查看, 比如常用的VS

  1. 只有数据成员的对象

    类实现如下:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    };

    对象大小及偏移:

    sizeof(Base1) 8
    offsetof(Base1, base1_1) 0
    offsetof(Base1, base1_2) 4

    可知对象布局:

    可以看到, 成员变量是按照定义的顺序来保存的, 最先声明的在最上边, 然后依次保存!
    类对象的大小就是所有成员变量大小之和.

  2. 没有虚函数的对象

    类实现如下:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        void foo(){}
    };

    结果如下:

    sizeof(Base1) 8
    offsetof(Base1, base1_1) 0
    offsetof(Base1, base1_2) 4

    和前面的结果是一样的? 不需要有什么疑问对吧?
    因为如果一个函数不是虚函数,那么他就不可能会发生动态绑定,也就不会对对象的布局造成任何影响.
    当调用一个非虚函数时, 那么调用的一定就是当前指针类型拥有的那个成员函数. 这种调用机制在编译时期就确定下来了.

  3. 拥有仅一个虚函数的类对象

    类实现如下:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        virtual void base1_fun1() {}
    };

    结果如下:

    sizeof(Base1) 12
    offsetof(Base1, base1_1) 4
    offsetof(Base1, base1_2) 8

    咦? 多了4个字节? 且 base1_1 和 base1_2 的偏移都各自向后多了4个字节!
    说明类对象的最前面被多加了4个字节的"东东", what's it?
    现在, 我们通过VS2013来瞧瞧类Base1的变量b1的内存布局情况:
    (由于我没有写构造函数, 所以变量的数据没有根据, 但虚函数是编译器为我们构造的, 数据正确!)
    (Debug模式下, 未初始化的变量值为0xCCCCCCCC, 即:-858983460)

    看到没? base1_1前面多了一个变量 __vfptr(常说的虚函数表vtable指针), 其类型为void**, 这说明它是一个void*指针(注意:不是数组).

    再看看[0]元素, 其类型为void*, 其值为 ConsoleApplication2.exe!Base1::base1_fun1(void), 这是什么意思呢? 如果对WinDbg比较熟悉, 那么应该知道这是一种惯用表示手法, 她就是指 Base1::base1_fun1() 函数的地址.

    可得, __vfptr的定义伪代码大概如下:

    void*   __fun[1] = { &Base1::base1_fun1 };
    const void**  __vfptr = &__fun[0];

    值得注意的是:

    1. sizeof(Base1) 12
      offsetof(__vfptr) 0
      offsetof(base1_1) 4
      offsetof(base1_2) 8
    2. 我为什么要提这样一个问题? 因为如果仅是一个指针的情况, 您就无法轻易地修改那个数组里面的内容, 因为她并不属于类对象的一部分.
      属于类对象的, 仅是一个指向虚函数表的一个指针__vfptr而已, 下一节我们将继续讨论这个问题.

    3. 注意到__vfptr前面的const修饰. 她修饰的是那个虚函数表, 而不是__vfptr.

    现在的对象布局如下:

    虚函数指针__vfptr位于所有的成员变量之前定义.

    注意到: 我并未在此说明__vfptr的具体指向, 只是说明了现在类对象的布局情况.
    接下来看一个稍微复杂一点的情况, 我将清楚地描述虚函数表的构成.

  4. 拥有多个虚函数的类对象

    和前面一个例子差不多, 只是再加了一个虚函数. 定义如下:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        virtual void base1_fun1() {}
        virtual void base1_fun2() {}
    };

    大小以及偏移信息如下:

    有情况!? 多了一个虚函数, 类对象大小却依然是12个字节!

    再来看看VS形象的表现:

    呀, __vfptr所指向的函数指针数组中出现了第2个元素, 其值为Base1类的第2个虚函数base1_fun2()的函数地址.

    现在, 虚函数指针以及虚函数表的伪定义大概如下:

    void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
    const void** __vfptr = &__fun[0];

    通过上面两张图表, 我们可以得到如下结论:

    1. 更加肯定前面我们所描述的: __vfptr只是一个指针, 她指向一个函数指针数组(即: 虚函数表)
    2. 增加一个虚函数, 只是简单地向该类对应的虚函数表中增加一项而已, 并不会影响到类对象的大小以及布局情况

    前面已经提到过: __vfptr只是一个指针, 她指向一个数组, 并且: 这个数组没有包含到类定义内部, 那么她们之间是怎样一个关系呢?
    不妨, 我们再定义一个类的变量b2, 现在再来看看__vfptr的指向:

    通过Watch 1窗口我们看到:

    1. b1和b2是类的两个变量, 理所当然, 她们的地址是不同的(见 &b1 和 &b2)
    2. 虽然b1和b2是类的两个变量, 但是: 她们的__vfptr的指向却是同一个虚函数表

    由此我们可以总结出:

    同一个类的不同实例共用同一份虚函数表, 她们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表.

    是时候该展示一下类对象的内存布局情况了:

    不出意外, 很清晰明了地展示出来了吧? :-) hoho~~

    那么问题就来了! 这个虚函数表保存在哪里呢? 其实, 我们无需过分追究她位于哪里, 重点是:

    1. 她是编译器在编译时期为我们创建好的, 只存在一份
    2. 定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表

  5. 单继承且本身不存在虚函数的继承类的内存布局

    前面研究了那么多啦, 终于该到研究继承类了! 先研究单继承!

    依然, 简单地定义一个继承类, 如下:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        virtual void base1_fun1() {}
        virtual void base1_fun2() {}
    };
    
    class Derive1 : public Base1
    {
    public:
        int derive1_1;
        int derive1_2;
    };

    我们再来看看现在的内存布局(定义为Derive1 d1):

    没错! 基类在上边, 继承类的成员在下边依次定义! 展开来看看:

    经展开后来看, 前面部分完全就是Base1的东西: 虚函数表指针+成员变量定义.
    并且, Base1的虚函数表的[0][1]两项还是其本身就拥有的函数: base1_fun1() 和 base1_fun2().

    现在类的布局情况应该是下面这样:

  6. 本身不存在虚函数(不严谨)但存在基类虚函数覆盖的单继承类的内存布局

    标题`本身不存在虚函数`的说法有些不严谨, 我的意思是说: 除经过继承而得来的基类虚函数以外, 自身没有再定义其它的虚函数.

    Ok, 既然存在基类虚函数覆盖, 那么来看看接下来的代码会产生何种影响:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        virtual void base1_fun1() {}
        virtual void base1_fun2() {}
    };
    
    class Derive1 : public Base1
    {
    public:
        int derive1_1;
        int derive1_2;
    
        // 覆盖基类函数
        virtual void base1_fun1() {}
    };

    可以看到, Derive1类 重写了Base1类的base1_fun1()函数, 也就是常说的虚函数覆盖. 现在是怎样布局的呢?

    特别注意我高亮的那一行: 原本是Base1::base1_fun1(), 但由于继承类重写了基类Base1的此方法, 所以现在变成了Derive1::base1_fun1()!

    那么, 无论是通过Derive1的指针还是Base1的指针来调用此方法, 调用的都将是被继承类重写后的那个方法(函数), 多态发生鸟!!!

    那么新的布局图:

  7. 定义了基类没有的虚函数的单继承的类对象布局

    说明一下: 由于前面一种情况只会造成覆盖基类虚函数表的指针, 所以接下来我不再同时讨论虚函数覆盖的情况.

    继续贴代码:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        virtual void base1_fun1() {}
        virtual void base1_fun2() {}
    };
    
    class Derive1 : public Base1
    {
    public:
        int derive1_1;
        int derive1_2;
    
        virtual void derive1_fun1() {}
    };

    和第5类不同的是多了一个自身定义的虚函数. 和第6类不同的是没有基类虚函数的覆盖.

    咦, 有没有发现问题? 表面上看来几乎和第5种情况完全一样? 为嘛呢?
    现在继承类明明定义了自身的虚函数, 但不见了??
    那么, 来看看类对象的大小, 以及成员偏移情况吧:

    居然没有变化!!! 前面12个字节是Base1的, 有没有觉得很奇怪?

    好吧, 既然表面上没办法了, 我们就只能从汇编入手了, 来看看调用derive1_fun1()时的代码:

    Derive1 d1;
    Derive1* pd1 = &d1;
    pd1->derive1_fun1();

    要注意: 我为什么使用指针的方式调用? 说明一下: 因为如果不使用指针调用, 虚函数调用是不会发生动态绑定的哦! 你若直接 d1.derive1_fun1();, 是不可能会发生动态绑定的, 但如果使用指针: pd1->derive1_fun1(); , 那么 pd1就无从知道她所指向的对象到底是Derive1 还是继承于Derive1的对象, 虽然这里我们并没有对象继承于Derive1, 但是她不得不这样做, 毕竟继承类不管你如何继承, 都不会影响到基类, 对吧?

    ; pd1->derive1_fun1();
    00825466  mov         eax,dword ptr [pd1]  
    00825469  mov         edx,dword ptr [eax]  
    0082546B  mov         esi,esp  
    0082546D  mov         ecx,dword ptr [pd1]  
    00825470  mov         eax,dword ptr [edx+8]  
    00825473  call        eax

    汇编代码解释:

    第2行: 由于pd1是指向d1的指针, 所以执行此句后 eax 就是d1的地址
    第3行: 又因为Base1::__vfptr是Base1的第1个成员, 同时也是Derive1的第1个成员, 那么: &__vfptr == &d1, clear? 所以当执行完 mov edx, dword ptr[eax] 后, edx就得到了__vfptr的值, 也就是虚函数表的地址.
    第5行: 由于是__thiscall调用, 所以把this保存到ecx中.
    第6行: 一定要注意到那个 edx+8, 由于edx是虚函数表的地址, 那么 edx+8将是虚函数表的第3个元素, 也就是__vftable[2]!!!
    第7行: 调用虚函数.

    结果:

    1. 现在我们应该知道内幕了! 继承类Derive1的虚函数表被加在基类的后面! 事实的确就是这样!
    2. 由于Base1只知道自己的两个虚函数索引[0][1], 所以就算在后面加上了[2], Base1根本不知情, 不会对她造成任何影响.
    3. 如果基类没有虚函数呢? 这个问题我们留到第9小节再来讨论!

    最新的类对象布局表示:

  8. 多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局

    真快, 该看看多继承了, 多继承很常见, 特别是接口类中!

    依然写点小类玩玩:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        virtual void base1_fun1() {}
        virtual void base1_fun2() {}
    };
    
    class Base2
    {
    public:
        int base2_1;
        int base2_2;
    
        virtual void base2_fun1() {}
        virtual void base2_fun2() {}
    };
    
    // 多继承
    class Derive1 : public Base1, public Base2
    {
    public:
        int derive1_1;
        int derive1_2;
    
        // 基类虚函数覆盖
        virtual void base1_fun1() {}
        virtual void base2_fun2() {}
    
        // 自身定义的虚函数
        virtual void derive1_fun1() {}
        virtual void derive1_fun2() {}
    };

    代码变得越来越长啦! 为了代码结构清晰, 我尽量简化定义.

    初步了解一下对象大小及偏移信息:

    貌似, 若有所思? 不管, 来看看VS再想:

    哇, 不摆了! 一丝不挂啊! :-)

    结论:

    1. 按照基类的声明顺序, 基类的成员依次分布在继承中.
    2. 注意被我高亮的那两行, 已经发生了虚函数覆盖!
    3. 我们自己定义的虚函数呢? 怎么还是看不见?!

    好吧, 继承反汇编, 这次的调用代码如下:

    Derive1 d1;
    Derive1* pd1 = &d1;
    pd1->derive1_fun2();

    反汇编代码如下:

    ; pd1->derive1_fun2();
    00995306  mov         eax,dword ptr [pd1]  
    00995309  mov         edx,dword ptr [eax]  
    0099530B  mov         esi,esp  
    0099530D  mov         ecx,dword ptr [pd1]  
    00995310  mov         eax,dword ptr [edx+0Ch]  
    00995313  call        eax

    解释下, 其实差不多:

    第2行: 取d1的地址
    第3行: 取Base1::__vfptr的值!!
    第6行: 0x0C, 也就是第4个元素(下标为[3])

    结论:

    Derive1的虚函数表依然是保存到第1个拥有虚函数表的那个基类的后面的.

    看看现在的类对象布局图:

    如果第1个基类没有虚函数表呢? 进入第9节!

  9. 如果第1个直接基类没有虚函数(表)

    这次的代码应该比上一个要稍微简单一些, 因为把第1个类的虚函数给去掉鸟!

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    };
    
    class Base2
    {
    public:
        int base2_1;
        int base2_2;
    
        virtual void base2_fun1() {}
        virtual void base2_fun2() {}
    };
    
    // 多继承
    class Derive1 : public Base1, public Base2
    {
    public:
        int derive1_1;
        int derive1_2;
    
        // 自身定义的虚函数
        virtual void derive1_fun1() {}
        virtual void derive1_fun2() {}
    };

    来看看VS的布局:

    这次相对前面一次的图来说还要简单啦! Base1已经没有虚函数表了! (真实情况并非完全这样, 请继续往下看!)

    现在的大小及偏移情况: 注意: sizeof(Base1) == 8;

    重点是看虚函数的位置, 进入函数调用(和前一次是一样的):

    Derive1 d1;
    Derive1* pd1 = &d1;
    pd1->derive1_fun2();

    反汇编调用代码:

    ; pd1->derive1_fun2();
    012E4BA6  mov         eax,dword ptr [pd1]  
    012E4BA9  mov         edx,dword ptr [eax]  
    012E4BAB  mov         esi,esp  
    012E4BAD  mov         ecx,dword ptr [pd1]  
    012E4BB0  mov         eax,dword ptr [edx+0Ch]  
    012E4BB3  call        eax

    这段汇编代码和前面一个完全一样!, 那么问题就来了! Base1 已经没有虚函数表了, 为什么还是把b1的第1个元素当作__vfptr呢?
    不难猜测: 当前的布局已经发生了变化, 有虚函数表的基类放在对象内存前面!? , 不过事实是否属实? 需要仔细斟酌.

    我们可以通过对基类成员变量求偏移来观察:

    可以看到:

    &d1==0x~d4
    &d1.Base1::__vfptr==0x~d4
    &d1.base2_1==0x~d8
    &d1.base2_2==0x~dc
    &d1.base1_1==0x~e0
    &d1.base1_2==0x~e4

    所以不难验证: 我们前面的推断是正确的, 谁有虚函数表, 谁就放在前面!

    现在类的布局情况:

    那么, 如果两个基类都没有虚函数表呢?

  10. What if 两个基类都没有虚函数表

    代码如下:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    };
    
    class Base2
    {
    public:
        int base2_1;
        int base2_2;
    };
    
    // 多继承
    class Derive1 : public Base1, public Base2
    {
    public:
        int derive1_1;
        int derive1_2;
    
        // 自身定义的虚函数
        virtual void derive1_fun1() {}
        virtual void derive1_fun2() {}
    };

    前面吃了个亏, 现在先来看看VS的基本布局:

    可以看到, 现在__vfptr已经独立出来了, 不再属于Base1和Base2!

    看看求偏移情况:

    Ok, 问题解决! 注意高亮的那两行, &d1==&d1.__vfptr, 说明虚函数始终在最前面!

    不用再废话, 相信大家对这种情况已经有底了.

    对象布局:

  11. 如果有三个基类: 虚函数表分别是有, 没有, 有!

    这种情况其实已经无需再讨论了, 作为一个完结篇....

    上代码:

    class Base1
    {
    public:
        int base1_1;
        int base1_2;
    
        virtual void base1_fun1() {}
        virtual void base1_fun2() {}
    };
    
    class Base2
    {
    public:
        int base2_1;
        int base2_2;
    };
    
    class Base3
    {
    public:
        int base3_1;
        int base3_2;
    
        virtual void base3_fun1() {}
        virtual void base3_fun2() {}
    };
    
    // 多继承
    class Derive1 : public Base1, public Base2, public Base3
    {
    public:
        int derive1_1;
        int derive1_2;
    
        // 自身定义的虚函数
        virtual void derive1_fun1() {}
        virtual void derive1_fun2() {}
    };

    只需要看看偏移就行了:

    只需知道: 谁有虚函数表, 谁就往前靠!

C++中父子对象指针间的转换与函数调用

讲了那么多布局方面的东东, 终于到了尾声, 好累呀!!!

通过前面的讲解内容, 大家至少应该明白了各类情况下类对象的内存布局了. 如果还不会.....呃..... !@#$%^&*

进入正题~

由于继承完全拥有父类的所有, 包括数据成员与虚函数表, 所以:把一个继承类强制转换为一个基类是完全可行的.

如果有一个Derive1的指针, 那么:

  • 得到Base1的指针: Base1* pb1 = pd1;
  • 得到Base2的指针: Base2* pb2 = pd1;
  • 得到Base3的指针: Base3* pb3 = pd1;

非常值得注意的是:

这是在基类与继承类之间的转换, 这种转换会自动计算偏移! 按照前面的布局方式!
也就是说: 在这里极有可能: pb1 != pb2 != pb3 ~~, 不要以为她们都等于 pd1!

至于函数调用, 我想, 不用说大家应该知道了:

  1. 如果不是虚函数, 直接调用指针对应的基本类的那个函数
  2. 如果是虚函数, 则查找虚函数表, 并进行后续的调用. 虚函数表在定义一个时, 编译器就为我们创建好了的. 所有的, 同一个类, 共用同一份虚函数表.

用C语言完全模拟C++虚函数表的实现与运作方式

如果对前面两大节的描述仔细了解了的话, 想用C语言来模拟C++的虚函数以及多态, 想必是轻而易举的事情鸟!

前提

但是, 话得说在前面, C++的编译器在生成类及对象的时候, 帮助我们完成了很多事件, 比如生成虚函数表!
但是, C语言编译器却没有, 因此, 很多事件我们必须手动来完成, 包括但不限于:

  1. 手动构造父子关系
  2. 手动创建虚函数表
  3. 手动设置__vfptr并指向虚函数表
  4. 手动填充虚函数表
  5. 若有虚函数覆盖, 还需手动修改函数指针
  6. 若要取得基类指针, 还需手动强制转换
  7. ......

总之, 要想用C语言来实现, 要写的代码绝对有点复杂.

C++原版调用

接下来, 我们都将以最后那个, 最繁杂的那个3个基类的实例来讲解, 但作了一些简化与改动:

  1. 用构造函数初始化成员变量
  2. 减少成员变量的个数
  3. 减少虚函数的个数
  4. 调用函数时产生相关输出
  5. Derive1增加一个基类虚函数覆盖

以下是对类的改动, 很少:

class Base1
{
public:
    Base1() : base1_1(11) {}
    int base1_1;
    virtual void base1_fun1() {
        std::cout << "Base1::base1_fun1()" << std::endl;
    }
};

class Base2
{
public:
    Base2() : base2_1(21) {}
    int base2_1;
};

class Base3
{
public:
    Base3() : base3_1(31) {}
    int base3_1;
    virtual void base3_fun1() {
        std::cout << "Base3::base3_fun1()" << std::endl;
    }
};

class Derive1 : public Base1, public Base2, public Base3
{
public:
    Derive1() : derive1_1(11) {}
    int derive1_1;

    virtual void base3_fun1() {
        std::cout << "Derive1::base3_fun1()" << std::endl;
    }
    virtual void derive1_fun1() {
            std::cout << "Derive1::derive1_fun1()" << std::endl;
    }
};

为了看到多态的效果, 我们还需要定义一个函数来看效果:

void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1)
{
    std::cout << "Base1::\n"
        << "    pb1->base1_1 = " << pb1->base1_1 << "\n"
        << "    pb1->base1_fun1(): ";
    pb1->base1_fun1();

    std::cout << "Base2::\n"
        << "    pb2->base2_1 = " << pb2->base2_1
        << std::endl;

    std::cout << "Base3::\n"
        << "    pb3->base3_1 = " << pb3->base3_1 << "\n"
        <<"    pb3->base3_fun1(): ";
    pb3->base3_fun1();

    std::cout << "Derive1::\n"
        << "    pd1->derive1_1 = " << pd1->derive1_1<< "\n"
        <<"    pd1->derive1_fun1(): ";
    pd1->derive1_fun1();
    std::cout<< "    pd1->base3_fun1(): ";
    pd1->base3_fun1();
    
    std::cout << std::endl;
}

调用方式如下:

Derive1 d1;
foo(&d1, &d1, &d1, &d1);

输出结果:

可以看到输出结果全部正确(当然了! :-), 哈哈~
同时注意到 pb3->base3_fun1() 的多态效果哦!

用C语言来模拟

必须要把前面的理解了, 才能看懂下面的代码!

为了有别于已经完成的C++的类, 我们分别在类前面加一个大写的C以示区分(平常大家都是习惯在C++写的类前面加C, 今天恰好反过来, 哈哈).

C语言无法实现的部分

C/C++是两个语言, 有些语言特性是C++专有的, 我们无法实现! 不过, 这里我是指调用约定, 我们应该把她排除在外.

对于类的成员函数, C++默认使用__thiscall, 也即this指针通过ecx传递, 这在C语言无法实现, 所以我们必须手动声明调用约定为:

  1. __stdcall, 就像微软的组件对象模型那样
  2. __cdecl, 本身就C语言的调用约定, 当然能使用了.

上面那种调用约定, 使用哪一种无关紧要, 反正不能使用__thiscall就行了.

因为使用了非__thiscall调用约定, 我们就必须手动传入this指针, 通过成员函数的第1个参数!

从最简单的开始: 实现 Base2

由于没有虚函数, 仅有成员变量, 这个当然是最好模拟的咯!

struct CBase2
{
    int base2_1;
};
有了虚函数表的Base1, 但没被覆盖

下面是Base1的定义, 要复杂一点了, 多一个__vfptr:

struct CBase1
{
    void** __vfptr;
    int base1_1;
};

因为有虚函数表, 所以还得单独为虚函数表创建一个结构体的哦!
但是, 为了更能清楚起见, 我并未定义前面所说的指针数组, 而是用一个包含一个或多个函数指针的结构体来表示!
因为数组能保存的是同一类的函数指针, 不太很友好! 
但他们的效果是完全一样的, 希望读者能够理解明白!

struct CBase1_VFTable
{
    void(__stdcall* base1_fun1)(CBase1* that);
};

注意: base1_fun1 在这里是一个指针变量!
注意: base1_fun1 有一个CBase1的指针, 因为我们不再使用__thiscall, 我们必须手动传入! Got it?

Base1的成员函数base1_fun1()我们也需要自己定义, 而且是定义成全局的:

void __stdcall base1_fun1(CBase1* that)
{
    std::cout << "base1_fun1()" << std::endl;
}
有虚函数覆盖的Base3

虚函数覆盖在这里并不能体现出来, 要在构造对象初始化的时候才会体现, 所以: base3其实和Base1是一样的.

struct CBase3
{
    void** __vfptr;
    int base3_1;
};

struct CBase3_VFTable
{
    void(__stdcall* base3_fun1)(CBase3* that);
};

Base3的成员函数:

void __stdcall base3_fun1(CBase3* that)
{
    std::cout << "base3_fun1()" << std::endl;
}
定义继承类CDerive1

相对前面几个类来说, 这个类要显得稍微复杂一些了, 因为包含了前面几个类的内容:

struct CDerive1
{
    CBase1 base1;
    CBase3 base3;
    CBase2 base2;

    int derive1_1;
};

特别注意: CBase123的顺序不能错!

另外: 由于Derive1本身还有虚函数表, 而且所以项是加到第一个虚函数表(CBase1)的后面的, 所以此时的CBase1::__vfptr不应该单单指向CBase1_VFTable, 而应该指向下面这个包含Derive1类虚函数表的结构体才行:

struct CBase1_CDerive1_VFTable
{
    void (__stdcall* base1_fun1)(CBase1* that);
    void(__stdcall* derive1_fun1)(CDerive1* that);
};

因为CDerive1覆盖了CBase3的base3_fun1()函数, 所以不能直接用Base3的那个表:

struct CBase3_CDerive1_VFTable
{
    void(__stdcall* base3_fun1)(CDerive1* that);
};

Derive1覆盖Base3::base3_fun1()的函数以及自身定义的derive1_fun1()函数:

void __stdcall base3_derive1_fun1(CDerive1* that)
{
    std::cout << "base3_derive1_fun1()" << std::endl;
}

void __stdcall derive1_fun1(CDerive1* that)
{
    std::cout << "derive1_fun1()" << std::endl;
}
构造各类的全局虚函数表

由于没有了编译器的帮忙, 在定义一个类对象时, 所有的初始化工作都只能由我们自己来完成了!

首先构造全局的, 被同一个类共同使用的虚函数表!

// CBase1 的虚函数表
CBase1_VFTable __vftable_base1;
__vftable_base1.base1_fun1 = base1_fun1;

// CBase3 的虚函数表
CBase3_VFTable __vftable_base3;
__vftable_base3.base3_fun1 = base3_fun1;

然后构造CDerive1和CBase1共同使用的虚函数表:

// CDerive1 和 CBase1 共用的虚函数表
CBase1_CDerive1_VFTable __vftable_base1_derive1;
__vftable_base1_derive1.base1_fun1 = base1_fun1;
__vftable_base1_derive1.derive1_fun1 = derive1_fun1;

再构造CDerive1覆盖CBase3后的虚函数表: 注意: 数覆盖会替换原来的函数指针

CBase3_CDerive1_VFTable __vftable_base3_derive1;
__vftable_base3_derive1.base3_fun1 = base3_derive1_fun1;
开始! 从CDerive1构造一个完整的Derive1类

先初始化成员变量与__vfptr的指向: 注意不是指错了!

CDerive1 d1;
d1.derive1 = 1;

d1.base1.base1_1 = 11;
d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);
    
d1.base2.base2_1 = 21;

d1.base3.base3_1 = 31;
d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);

由于目前的CDerive1是我们手动构造的, 不存在真正语法上的继承关系, 如要得到各基类指针, 我们就不能直接来取, 必须手动根据偏移计算:

char* p = reinterpret_cast<char*>(&d1);
Base1* pb1 = reinterpret_cast<Base1*>(p + 0);
Base2* pb2 = reinterpret_cast<Base2*>(p + sizeof(CBase1) + sizeof(CBase3));
Base3* pb3 = reinterpret_cast<Base3*>(p + sizeof(CBase1));
Derive1* pd1 = reinterpret_cast<Derive1*>(p);

真正调用:

foo(pb1, pb2, pb3, pd1);

调用结果:

结果相当正确!!!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

c++虚函数实现机制及内存模型 的相关文章

  • 用于代数简化和求解的 C# 库 [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 网络上有很多代数求解器和简化器 例如 algebra com 上不错的代数求解器和简化器 然而 我正在
  • 每个托管线程是否都有自己对应的本机线程?

    我想知道是否在 Net 中创建托管线程 通过调用Thread Start 导致在后台创建一个本机线程 那么托管线程是否有对应的本机线程呢 如果是 当托管线程等待或睡眠时 是否意味着相应的本机线程也在等待或睡眠 是的 NET 线程映射到所有当
  • 如何在c++中读取pcap文件来获取数据包信息?

    我想用 C 编写一个程序来读取 pcap 文件并获取数据包的信息 例如 len sourc ip flags 等 现在我找到了如下代码 我认为它会帮助我获取信息 但是我有一些疑问 首先我想知道应该将哪个库添加到我的程序中 然后什么是 pca
  • 计算 Richtextbox 中所有单词的最有效方法是什么?

    我正在编写一个文本编辑器 需要提供实时字数统计 现在我正在使用这个扩展方法 public static int WordCount this string s s s TrimEnd if String IsNullOrEmpty s re
  • MVC 在布局代码之前执行视图代码并破坏我的脚本顺序

    我正在尝试将所有 javascript 包含内容移至页面底部 我正在将 MVC 与 Razor 一起使用 我编写了一个辅助方法来注册脚本 它按注册顺序保留脚本 并排除重复的内容 Html RegisterScript scripts som
  • C中的malloc内存分配方案

    我在 C 中尝试使用 malloc 发现 malloc 在分配了一些内存后浪费了一些空间 下面是我用来测试 malloc 的一段代码 include
  • 获取两个工作日之间的天数差异

    这听起来很简单 但我不明白其中的意义 那么获取两次之间的天数的最简单方法是什么DayOfWeeks当第一个是起点时 如果下一个工作日较早 则应考虑在下周 The DayOfWeek 枚举 http 20 20 5B1 5D 3a 20htt
  • 为什么调用非 const 成员函数而不是 const 成员函数?

    为了我的目的 我尝试包装一些类似于 Qt 共享数据指针的东西 经过测试 我发现当应该调用 const 函数时 会选择它的非 const 版本 我正在使用 C 0x 选项进行编译 这是一个最小的代码 struct Data int x con
  • 我可以使用 moq Mock 来模拟类而不是接口吗?

    正在经历https github com Moq moq4 wiki Quickstart https github com Moq moq4 wiki Quickstart 我看到它 Mock 一个接口 我的遗留代码中有一个没有接口的类
  • 如何禁用 fread() 中的缓冲?

    我正在使用 fread 和 fwrite 读取和写入套接字 我相信这些函数用于缓冲输入和输出 有什么方法可以在仍然使用这些功能的同时禁用缓冲吗 Edit 我正在构建一个远程桌面应用程序 远程客户端似乎 落后于服务器 我不知道可能是什么原因
  • C# 中的合并运算符?

    我想我记得看到过类似的东西 三元运算符 http msdn microsoft com en us library ty67wk28 28VS 80 29 aspx在 C 中 它只有两部分 如果变量值不为空 则返回变量值 如果为空 则返回默
  • AES 128 CBC 蒙特卡罗测试

    我正在 AES 128 CBC 上执行 MCT 如中所述http csrc nist gov groups STM cavp documents aes AESAVS pdf http csrc nist gov groups STM ca
  • “接口”类似于 boost::bind 的语义

    我希望能够将 Java 的接口语义与 C 结合起来 起初 我用过boost signal为给定事件回调显式注册的成员函数 这非常有效 但后来我发现一些函数回调池是相关的 因此将它们抽象出来并立即注册所有实例的相关回调是有意义的 但我了解到的
  • 如何设置 log4net 每天将我的文件记录到不同的文件夹中?

    我想将每天的所有日志保存在名为 YYYYMMdd 的文件夹中 log4net 应该根据系统日期时间处理创建新文件夹 我如何设置它 我想将一天中的所有日志保存到 n 个 1MB 的文件中 我不想重写旧文件 但想真正拥有一天中的所有日志 我该如
  • 无法接收 UDP Windows RT

    我正在为 Windows 8 RT 编写一个 Windows Store Metro Modern RT 应用程序 需要在端口 49030 上接收 UDP 数据包 但我似乎无法接收任何数据包 我已按照使用教程进行操作DatagramSock
  • WebSocket安全连接自签名证书

    目标是一个与用户电脑上安装的 C 应用程序交换信息的 Web 应用程序 客户端应用程序是 websocket 服务器 浏览器是 websocket 客户端 最后 用户浏览器中的 websocket 客户端通过 Angular 持久创建 并且
  • 我的班级应该订阅自己的公共活动吗?

    我正在使用 C 3 0 遵循标准事件模式我有 public event EventHandler
  • Oracle Data Provider for .NET 不支持 Oracle 19.0.48.0.0

    我们刚刚升级到 Oracle 19c 19 3 0 所有应用程序都停止工作并出现以下错误消息 Oracle Data Provider for NET 不支持 Oracle 19 0 48 0 0 我将 Oracle ManagedData
  • 如何从 ODBC 连接获取可用表的列表?

    在 Excel 中 我可以转到 数据 gt 导入外部数据 gt 导入数据 然后选择要使用的数据源 然后在提供登录信息后 它会给我一个表格列表 我想知道如何使用 C 以编程方式获取该列表 您正在查询什么类型的数据源 SQL 服务器 使用权 看
  • 当从finally中抛出异常时,Catch块不会被评估

    出现这个问题的原因是之前在 NET 4 0 中运行的代码在 NET 4 5 中因未处理的异常而失败 部分原因是 try finallys 如果您想了解详细信息 请阅读更多内容微软连接 https connect microsoft com

随机推荐

  • 为什么离不开 Stackoverflow

    作为一名程序员 如果没有听过 Stackoverflow 那么你最好去面壁思过一下 程序员最需要阅读的一本编程书籍 其实编程书留下这本就够了 那些还没有读过这本书的程序员 是时候买一本了 如果还在犹豫 那么先看下这篇文章 看看为什么离不开
  • linux创建链接命令

    1 软链接 符号链接 1 软链接文件有类似于Windows的快捷方式 2 在符号连接中 文件实际上是一个文本文件 其中包含的有另一文件的位置信息 3 它只会在你选定的位置上生成一个文件的镜像 不会占用磁盘空间 linux创建链接软命令 具体
  • C语言调用C++函数

    前阵子被问及一个在C中如何调用C 函数的问题 当时简单回答是将函数用extern C 声明 当被问及如何将类内成员函数声明时 一时语塞 后来网上查了下 网上有一翻译C 之父的文章可以作为解答 遂拿来Mark一下 将 C 函数声明为 exte
  • JS 5种遍历对象的方式

    From https blog csdn net qq 53225741 article details 127073295 我根据阮老师的 ES6标准入门 学习并总结了七种遍历对象的方法 我会将分别介绍这七种方法并进行详细的区分 并将从属
  • Linux Ubuntu 能PING IP但不能PING主机域名的解决方法

    vi etc nsswitch conf hosts files dns networks files 改成 hosts files dns wins networks files 如果不一样的话 就在hosts 原来那行后面加个wins
  • Vue2转Vue3快速上手第一篇(共两篇)

    Vue3 v2 v3的学习成本不高 只要有v2基础 基本可以上手vue3 一 setup语法 setup中不能访问v2的配置比如data methods等 二 ref响应数据 使用ref可以创建一个对象 可以是基本类型 也可以是对象 例如
  • SpringBoot获取resources 目录下的文件的方式

    SpringBoot获取resources 目录下的文件的方式在Spring Boot项目中 读取resources目录下文件的方式是非常常见的操作 为了确保项目的稳定性和可靠性 我们需要采取一种高效的方法来获取这些文件 因此 在本文中 我
  • overloading与overriding的区别

    1 overloading 重载 1 方法重载是让类以一种统一的方式处理不同类型数据的手段 多个同名函数同时存在 具有不同参数个数 类型 重载是一个类中多态性的表现 2 java方法重载就是在同一个类中创建多个具有相同的方法名 但是参数类型
  • MAC M1安装VMware 安装windows11

    目录 前言 一 安装包列表 二 VMware安装Windows11过程 总结 前言 最近想着给自己的mac安装windows虚拟机 因为mac是m1芯片的 所以也是从网上找了很多资料 用PD安装了Windows11 在找资料的时候发现VM也
  • Hbuild X 下载以及插件安装

    1 下载 下载地址 https www dcloud io 2 进入Hbuilder 官方网站 3 下载HBuilder 点击下载按钮 Download for Windows 点击后会直接下载 也可以鼠标移动到 more 选择对应的版本点
  • VC使用ActiveX控件常见问题

    转自 http lingchuangsong blog 163 com blog static 126932322008631104133309 一方面 它表示将你联系到Microsoft Internet和业界的新技术的小型快速的可重用组
  • 大数据应用——Hadoop运行模式(本地运行)

    Hadoop运行模式包括 本地模式 伪分布式模式以及完全分布式模式 Hadoop官方网站 http hadoop apache org 4 1本地运行模式 4 1 1 官方Grep案例 1 创建在hadoop 2 7 1文件下面创建一个in
  • pycharm注释、查看函数用法快捷键

    单行或多行注释 选中代码 ctrl 查询函数用法 ctrl 鼠标左击函数名 便可以直接进入原文件查看此函数的定义 自动填充空格 ctrl alt L 将光标置于需要调整的代码行 或者选择一个区域 按下快捷键后 代码会自动填充空格 自动对齐代
  • matlab figure函数的用法

    https blog csdn net qq 30387863 article details 80301996
  • (三) 区块链数据结构 – 交易

    区块由交易组成 区块体中包含若干项交易数据 交易 交易主要包含两类数据 交易输入和交易输出 交易输入用来指明钱的来源 交易输出用来指明钱的去向 除了交易输入和交易输出外 交易中还包含版本号和锁定时间 交易数据的存储结构如下 交易对象中的各个
  • Flink State 和 Fault Tolerance详解

    有状态操作或者操作算子在处理DataStream的元素或者事件的时候需要存储计算的中间状态 这就使得状态在整个Flink的精细化计算中有着非常重要的地位 记录数据从某一个过去时间点到当前时间的状态信息 以每分钟 小时 天汇总事件时 状态将保
  • 平面离散点集Delaunay三角化

    文章目录 定义 准则 特性 算法 1 逐点插入法 2 分治算法 3 生长算法 对比 参考 定义 在数学和计算几何中 三角化就是对于给定的平面中的离散点集P 生成三角形集合T的过程 一般来说给定一个点集 往往存在不止一个三角剖分 其中基于 D
  • 大数据时代下对NoSQL数据库的理解

    Web 2 0时代的到来 关系数据库越来越不能满足互联网应用的需求 导致了NoSQL的兴起 NoSQL数据库在大数据领域里越来越受欢迎 数据的高并发读写 数据的高可用性 海量数据存储 海量数据的实时分析 文档型数据库 代表 MongoDB
  • 边缘计算在物联网领域的研究与分析

    本文首发于 5G工业互联 作者黄泽龙 摘 要 物联网的发展开启了万物互联时代 设备的爆炸式增长和应用的多样化带来了海量数据 对传输带宽 时效性 异构接入等提出了新要求 边缘计算在靠近数据源侧进行数据处理 有效地减少数据传输量 降低服务响应时
  • c++虚函数实现机制及内存模型

    前言 大家都应该知道C 的精髓是虚函数吧 虚函数带来的好处就是 可以定义一个基类的指针 其指向一个继承类 当通过基类的指针去调用函数时 可以在运行时决定该调用基类的函数还是继承类的函数 虚函数是实现多态 动态绑定 接口函数的基础 可以说 没