447

我想知道什么是“虚拟基类”以及它的含义。

让我举个例子:

class Foo
{
public:
    void DoSomething() { /* ... */ }
};

class Bar : public virtual Foo
{
public:
    void DoSpecific() { /* ... */ }
};
4

11 回答 11

571

虚拟继承中使用的虚拟基类是一种在使用多重继承时防止给定类的多个“实例”出现在继承层次结构中的方法。

考虑以下场景:

class A { public: void Foo() {} };
class B : public A {};
class C : public A {};
class D : public B, public C {};

上面的类层次结构导致“可怕的钻石”看起来像这样:

  A
 / \
B   C
 \ /
  D

D 的一个实例将由包含 A 的 B 和还包含 A 的 C 组成。所以你有两个 A 的“实例”(为了更好的表达)。

当您遇到这种情况时,您可能会产生歧义。当你这样做时会发生什么:

D d;
d.Foo(); // is this B's Foo() or C's Foo() ??

虚拟继承就是为了解决这个问题。当您在继承类时指定 virtual 时,您是在告诉编译器您只需要一个实例。

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

这意味着层次结构中只包含一个 A 的“实例”。因此

D d;
d.Foo(); // no longer ambiguous

这是一个小总结。有关更多信息,请阅读thisthis这里也有一个很好的例子。

于 2008-08-22T01:45:02.770 回答
281

关于内存布局

附带说明一下,Dreaded Diamond 的问题是基类多次出现。因此,通过常规继承,您相信您拥有:

  A
 / \
B   C
 \ /
  D

但是在内存布局中,您有:

A   A
|   |
B   C
 \ /
  D

这解释了为什么在 callD::foo()时会出现歧义问题。但真正的问题是当你想使用A. 例如,假设我们有:

class A
{
    public :
       foo() ;
       int m_iValue ;
} ;

当您尝试访问m_iValuefromD时,编译器会抗议,因为在层次结构中,它会看到两个m_iValue,而不是一个。而且,如果您修改一个,例如B::m_iValue(即 的A::m_iValue父级B),C::m_iValue则不会被修改(即 的A::m_iValue父级C)。

这就是虚拟继承派上用场的地方,因为有了它,您将回到真正的菱形布局,不仅只有一种foo()方法,而且只有一种m_iValue

会出什么问题?

想象:

  • A有一些基本特征。
  • B添加一些很酷的数据数组(例如)
  • C添加了一些很酷的功能,例如观察者模式(例如, on m_iValue)。
  • D继承自BC,因此继承自A

对于普通继承,修改m_iValuefromD是模棱两可的,必须解决这个问题。即使是,里面也有两个m_iValuesD所以你最好记住并同时更新两个。

使用虚拟继承,修改m_iValuefromD是可以的......但是......假设你有D. 通过它的C接口,你附加了一个观察者。并且通过它的B界面,你更新cool数组,它的副作用是直接改变m_iValue...

由于更改m_iValue是直接完成的(不使用虚拟访问器方法),因此C不会调用“监听”通过的观察者,因为实现监听的代码在 中C,并且B不知道...

结论

如果您的层次结构中有菱形,则意味着您有 95% 的概率在所述层次结构中做错了什么。

于 2008-09-21T23:06:14.070 回答
37

用虚拟基解释多重继承需要了解 C++ 对象模型。清楚地解释这个主题最好在一篇文章中完成,而不是在评论框中。

我发现解决我对这个主题的所有疑问的最好的、可读的解释是这篇文章:http ://www.phpcompiler.org/articles/virtualinheritance.html

阅读完之后,您真的不需要阅读有关该主题的任何其他内容(除非您是编译器作者)...

于 2009-06-13T22:59:54.637 回答
10

虚拟基类是无法实例化的类:您无法从中创建直接对象。

我认为你混淆了两个非常不同的事情。虚拟继承与抽象类不同。虚拟继承修改了函数调用的行为;有时它会解决否则会模棱两可的函数调用,有时它将函数调用处理推迟到一个类,而不是在非虚拟继承中期望的类。

于 2008-08-22T01:47:36.913 回答
7

我想补充一下 OJ 的友好说明。

虚拟继承并非没有代价。就像所有虚拟事物一样,您的性能也会受到影响。有一种方法可能不太优雅。

您可以向钻石添加另一层,而不是通过虚拟导出来破坏钻石,以获得如下效果:

   B
  / \
D11 D12
 |   |
D21 D22
 \   /
  DD

没有一个类是虚拟继承的,都是公开继承的。然后类 D21 和 D22 将隐藏虚拟函数 f(),这对于 DD 来说是模棱两可的,也许是通过声明函数私有。他们各自定义了一个包装函数,分别是 f1() 和 f2(),每个调用类本地(私有)f(),从而解决冲突。如果需要 D11::f(),则 DD 类调用 f1(),如果需要 D12::f(),则调用 f2()。如果您内联定义包装器,您可能会得到零开销。

当然,如果您可以更改 D11 和 D12,那么您可以在这些类中执行相同的技巧,但通常情况并非如此。

于 2008-08-22T02:03:04.067 回答
6

除了已经说过的关于多重继承和虚拟继承之外,Dobb 博士的期刊上有一篇非常有趣的文章:多重继承被认为是有用的

于 2008-09-22T00:58:41.037 回答
3

钻石继承可运行使用示例

这个例子展示了如何在典型场景中使用虚拟基类:解决菱形继承问题。

考虑以下工作示例:

主文件

#include <cassert>

class A {
    public:
        A(){}
        A(int i) : i(i) {}
        int i;
        virtual int f() = 0;
        virtual int g() = 0;
        virtual int h() = 0;
};

class B : public virtual A {
    public:
        B(int j) : j(j) {}
        int j;
        virtual int f() { return this->i + this->j; }
};

class C : public virtual A {
    public:
        C(int k) : k(k) {}
        int k;
        virtual int g() { return this->i + this->k; }
};

class D : public B, public C {
    public:
        D(int i, int j, int k) : A(i), B(j), C(k) {}
        virtual int h() { return this->i + this->j + this->k; }
};

int main() {
    D d = D(1, 2, 4);
    assert(d.f() == 3);
    assert(d.g() == 5);
    assert(d.h() == 7);
}

编译并运行:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

如果我们删除virtual成:

class B : public virtual A

我们会得到一堵关于 GCC 无法解析通过 A 继承两次的 D 成员和方法的错误:

main.cpp:27:7: warning: virtual base ‘A’ inaccessible in ‘D’ due to ambiguity [-Wextra]
   27 | class D : public B, public C {
      |       ^
main.cpp: In member function ‘virtual int D::h()’:
main.cpp:30:40: error: request for member ‘i’ is ambiguous
   30 |         virtual int h() { return this->i + this->j + this->k; }
      |                                        ^
main.cpp:7:13: note: candidates are: ‘int A::i’
    7 |         int i;
      |             ^
main.cpp:7:13: note:                 ‘int A::i’
main.cpp: In function ‘int main()’:
main.cpp:34:20: error: invalid cast to abstract class type ‘D’
   34 |     D d = D(1, 2, 4);
      |                    ^
main.cpp:27:7: note:   because the following virtual functions are pure within ‘D’:
   27 | class D : public B, public C {
      |       ^
main.cpp:8:21: note:    ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:9:21: note:    ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:34:7: error: cannot declare variable ‘d’ to be of abstract type ‘D’
   34 |     D d = D(1, 2, 4);
      |       ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:35:14: error: request for member ‘f’ is ambiguous
   35 |     assert(d.f() == 3);
      |              ^
main.cpp:8:21: note: candidates are: ‘virtual int A::f()’
    8 |         virtual int f() = 0;
      |                     ^
main.cpp:17:21: note:                 ‘virtual int B::f()’
   17 |         virtual int f() { return this->i + this->j; }
      |                     ^
In file included from /usr/include/c++/9/cassert:44,
                 from main.cpp:1:
main.cpp:36:14: error: request for member ‘g’ is ambiguous
   36 |     assert(d.g() == 5);
      |              ^
main.cpp:9:21: note: candidates are: ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
main.cpp:24:21: note:                 ‘virtual int C::g()’
   24 |         virtual int g() { return this->i + this->k; }
      |                     ^
main.cpp:9:21: note:                 ‘virtual int A::g()’
    9 |         virtual int g() = 0;
      |                     ^
./main.out

在 GCC 9.3.0、Ubuntu 20.04 上测试。

于 2016-12-07T19:52:13.310 回答
2

常规继承

对于典型的 3 级非菱形非虚拟继承继承,当您实例化一个新的最派生对象时,new将调用该对象所需的堆上对象的大小,该大小由编译器从类类型解析并传递给 new .

new有一个签名:

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)

并调用malloc,返回 void 指针

然后将此地址传递给最派生对象的构造函数,该构造函数将立即调用中间构造函数,然后中间构造函数将立即调用基构造函数。然后,基础在对象的开头存储指向其虚拟表的指针,然后在其后存储其属性。然后返回到中间构造函数,该构造函数将其虚拟表指针存储在同一位置,然后将其属性存储在基本构造函数存储的属性之后。然后它返回到最派生的构造函数,该构造函数将指向其虚拟表的指针存储在同一位置,然后将其属性存储在中间构造函数将存储的属性之后。

因为虚表指针被覆盖,所以虚表指针最终总是成为最派生类之一。虚拟性传播到最派生类,因此如果一个函数在中间类中是虚拟的,那么它将在最派生类中是虚拟的,而不是在基类中。如果您将最派生类的实例多态转换为指向基类的指针,则编译器不会将此解析为对虚拟表的间接调用,而是直接调用该函数A::function()。如果一个函数对于您将其强制转换为的类型是虚拟的,那么它将解析为对虚拟表的调用,该虚拟表始终是最派生类的调用。如果该类型不是虚拟的,那么它只会调用Type::function()并将对象指针传递给它,强制转换为 Type。

实际上,当我说指向它的虚拟表的指针时,它实际上总是在虚拟表中偏移 16。

vtable for Base:
        .quad   0
        .quad   typeinfo for Base
        .quad   Base::CommonFunction()
        .quad   Base::VirtualFunction()

pointer is typically to the first function i.e. 

        mov     edx, OFFSET FLAT:vtable for Base+16

virtual如果它在派生较少的类中是虚拟的,则在派生较多的类中不再需要它,因为它在派生最多的类的方向上向下传播。但它可以用来证明该函数确实是一个虚函数,而不必检查它所继承的类的类型定义。当一个函数被声明为 virtual 时,从那时起,只使用继承链中的最后一个实现,但在此之前,如果对象被强制转换为之前的类的类型,它仍然可以非虚拟地使用。定义该方法的继承链。它可以在虚拟开始之前在链中的多个类中非虚拟定义,然后为该名称和签名的方法开始,并且在引用时它们将使用自己的方法(如果链中该定义之后的所有类没有自己的定义,则它们将使用该定义,而不是始终使用最终定义的虚拟)。当一个方法被声明为虚拟方法时,它必须在该类或继承链中更派生的类中实现,以便为使用而构造的完整对象。

override是另一个编译器守卫,它说这个函数正在覆盖某些东西,如果不是,则抛出编译器错误。

= 0表示这是一个抽象函数

final防止虚函数在更派生的类中再次实现,并确保最派​​生类的虚表包含该类的最终函数。

= default在文档中明确说明编译器将使用默认实现

= delete如果尝试调用 this,则给出编译器错误

如果您调用非虚拟函数,它将解析为正确的方法定义,而无需通过虚拟表。如果您调用一个在继承类中具有最终定义的虚函数,那么如果您在调用该方法时没有将对象指针转换为该类型,它将使用其虚表并将子对象自动传递给它。如果您在该类型的指针上调用在最派生类中定义的虚函数,那么它将使用其虚表,该表将是对象开头的表。如果您在继承类型的指针上调用它,并且该函数在该类中也是虚拟的,那么它将使用该子对象的 vtable 指针,在第一个子对象的情况下,该指针与最派生类的指针相同,

将对象与引用运算符一起使用而不是通过指针(解引用运算符)会破坏多态性,并将虚方法视为常规方法。这是因为由于切片,非指针类型上的多态强制转换不会发生。

虚拟继承

考虑

class Base
  {
      int a = 1;
      int b = 2;
  public:
      void virtual CommonFunction(){} ; //define empty method body
      void virtual VirtualFunction(){} ;
  };


class DerivedClass1: virtual public Base
  {
      int c = 3;
  public:
    void virtual DerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
  };
  
  class DerivedClass2 : virtual public Base
 {
     int d = 4;
 public:
     //void virtual DerivedCommonFunction(){} ;    
     void virtual VirtualFunction(){} ;
     void virtual DerivedCommonFunction2(){} ;
 };

class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
 {
   int e = 5;
 public:
     void virtual DerivedDerivedCommonFunction(){} ;
     void virtual VirtualFunction(){} ;
 };
 
 int main () {
   DerivedDerivedClass* d = new DerivedDerivedClass;
   d->VirtualFunction();
   d->DerivedCommonFunction();
   d->DerivedCommonFunction2();
   d->DerivedDerivedCommonFunction();
   ((DerivedClass2*)d)->DerivedCommonFunction2();
   ((Base*)d)->VirtualFunction();
 }

在没有虚拟继承低音类的情况下,您将获得一个如下所示的对象:

而不是这个:

即将有2个基础对象。

在上面的虚拟菱形继承情况下,在new被调用之后,它将为对象分配的空间的地址传递给最派生的构造函数DerivedDerivedClass::DerivedDerivedClass(),它首先调用Base::Base(),将它的vtable写入基的专用子对象,然后DerivedDerivedClass::DerivedDerivedClass()调用DerivedClass1::DerivedClass1(),写入它的指向其子对象的虚拟表指针,并通过咨询传递的 VTT 在对象末尾覆盖基本子对象的指针,然后调用DerivedClass1::DerivedClass1()执行相同的操作,最后DerivedDerivedClass::DerivedDerivedClass()用该继承类的虚拟表指针覆盖所有 3 个指针。这不是(如上图所示)DerivedDerivedClass::DerivedDerivedClass()调用DerivedClass1::DerivedClass1()和调用Base::Base()(它覆盖了虚拟指针),返回,将地址偏移到下一个子对象,调用DerivedClass2::DerivedClass2()然后那个也调用Base::Base(),覆盖那个虚拟指针,返回然后DerivedDerivedClass构造函数用它的虚拟表指针覆盖两个虚拟指针(在这种情况下,虚拟最派生构造函数的表包含 2 个子表而不是 3)。

以下都是在调试模式下编译的 -O0 所以会有多余的汇编

main:
.LFB8:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     edi, 48 //pass size to new
        call    operator new(unsigned long) //call new
        mov     rbx, rax  //move the address of the allocation to rbx
        mov     rdi, rbx  //move it to rdi i.e. pass to the call
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor] //construct on this address
        mov     QWORD PTR [rbp-24], rbx  //store the address of the object on the stack as the d pointer variable on -O0, will be optimised off on -Ofast if the address of the pointer itself isn't taken in the code, because this address does not need to be on the stack, it can just be passed in a register to the subsequent methods

顺便说一句,如果代码是DerivedDerivedClass d = DerivedDerivedClass(),则main函数将如下所示:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48 // make room for and zero 48 bytes on the stack for the 48 byte object, no extra padding required as the frame is 64 bytes with `rbp` and return address of the function it calls (no stack params are passed to any function it calls), hence rsp will be aligned by 16 assuming it was aligned at the start of this frame
        mov     QWORD PTR [rbp-48], 0
        mov     QWORD PTR [rbp-40], 0
        mov     QWORD PTR [rbp-32], 0
        mov     QWORD PTR [rbp-24], 0
        mov     QWORD PTR [rbp-16], 0
        mov     QWORD PTR [rbp-8], 0
        lea     rax, [rbp-48] // load the address of the cleared 48 bytes
        mov     rdi, rax // pass the address as a pointer to the 48 bytes cleared as the first parameter to the constructor
        call    DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]
        //address is not stored on the stack because the object is used directly -- there is no pointer variable -- d refers to the object on the stack as opposed to being a pointer

回到原来的例子,DerivedDerivedClass构造函数:

DerivedDerivedClass::DerivedDerivedClass() [complete object constructor]:
.LFB20:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
.LBB5:
        mov     rax, QWORD PTR [rbp-8] // object address now in rax 
        add     rax, 32 //increment address by 32
        mov     rdi, rax // move object address+32 to rdi i.e. pass to call
        call    Base::Base() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+8 //move address of VTT+8 to edx
        mov     rsi, rdx //pass VTT+8 address as 2nd parameter 
        mov     rdi, rax //object address as first (DerivedClass1 subobject)
        call    DerivedClass1::DerivedClass1() [base object constructor]
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        add     rax, 16  //increment object address by 16
        mov     edx, OFFSET FLAT:VTT for DerivedDerivedClass+24  //store address of VTT+24 in edx
        mov     rsi, rdx //pass address of VTT+24 as second parameter
        mov     rdi, rax //address of DerivedClass2 subobject as first
        call    DerivedClass2::DerivedClass2() [base object constructor]
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+24 //move this to edx
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        mov     QWORD PTR [rax], rdx. //store address of vtable for DerivedDerivedClass+24 at the start of the object
        mov     rax, QWORD PTR [rbp-8] // object address now in rax
        add     rax, 32  // increment object address by 32
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+120 //move this to edx
        mov     QWORD PTR [rax], rdx  //store vtable for DerivedDerivedClass+120 at object+32 (Base) 
        mov     edx, OFFSET FLAT:vtable for DerivedDerivedClass+72 //store this in edx
        mov     rax, QWORD PTR [rbp-8] //move object address to rax
        mov     QWORD PTR [rax+16], rdx //store vtable for DerivedDerivedClass+72 at object+16 (DerivedClass2)
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+28], 5 // stores e = 5 in the object
.LBE5:
        nop
        leave
        ret

构造函数使用指向对象偏移量 32 的指针进行DerivedDerivedClass调用Base::Base()。 Base 将指向其虚拟表的指针存储在它接收到的地址及其后面的成员中。

Base::Base() [base object constructor]:
.LFB11:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //stores address of object on stack (-O0)
.LBB2:
        mov     edx, OFFSET FLAT:vtable for Base+16  //puts vtable for Base+16 in edx
        mov     rax, QWORD PTR [rbp-8] //copies address of object from stack to rax
        mov     QWORD PTR [rax], rdx  //stores it address of object
        mov     rax, QWORD PTR [rbp-8] //copies address of object on stack to rax again
        mov     DWORD PTR [rax+8], 1 //stores a = 1 in the object
        mov     rax, QWORD PTR [rbp-8] //junk from -O0
        mov     DWORD PTR [rax+12], 2  //stores b = 2 in the object
.LBE2:
        nop
        pop     rbp
        ret

DerivedDerivedClass::DerivedDerivedClass()然后DerivedClass1::DerivedClass1()使用指向对象偏移量 0 的指针进行调用,并传递VTT for DerivedDerivedClass+8

DerivedClass1::DerivedClass1() [base object constructor]:
.LFB14:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi //address of object
        mov     QWORD PTR [rbp-16], rsi  //address of VTT+8
.LBB3:
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rdx, QWORD PTR [rax]     //address of DerivedClass1-in-DerivedDerivedClass+24 now in rdx
        mov     rax, QWORD PTR [rbp-8]   //address of object now in rax
        mov     QWORD PTR [rax], rdx     //store address of DerivedClass1-in-.. in the object
        mov     rax, QWORD PTR [rbp-8]  // address of object now in rax
        mov     rax, QWORD PTR [rax]    //address of DerivedClass1-in.. now implicitly in rax
        sub     rax, 24                 //address of DerivedClass1-in-DerivedDerivedClass+0 now in rax
        mov     rax, QWORD PTR [rax]    //value of 32 now in rax
        mov     rdx, rax                // now in rdx
        mov     rax, QWORD PTR [rbp-8]  //address of object now in rax
        add     rdx, rax                //address of object+32 now in rdx
        mov     rax, QWORD PTR [rbp-16]  //address of VTT+8 now in rax
        mov     rax, QWORD PTR [rax+8]   //derference VTT+8+8; address of DerivedClass1-in-DerivedDerivedClass+72 (Base::CommonFunction()) now in rax
        mov     QWORD PTR [rdx], rax     //store at address object+32 (offset to Base)
        mov     rax, QWORD PTR [rbp-8]  //store address of object in rax, return
        mov     DWORD PTR [rax+8], 3    //store its attribute c = 3 in the object
.LBE3:
        nop
        pop     rbp
        ret
VTT for DerivedDerivedClass:
        .quad   vtable for DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+24 //(DerivedClass1 uses this to write its vtable pointer)
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+72 //(DerivedClass1 uses this to overwrite the base vtable pointer)
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+72
        .quad   vtable for DerivedDerivedClass+120 // DerivedDerivedClass supposed to use this to overwrite Bases's vtable pointer
        .quad   vtable for DerivedDerivedClass+72 // DerivedDerivedClass supposed to use this to overwrite DerivedClass2's vtable pointer
//although DerivedDerivedClass uses vtable for DerivedDerivedClass+72 and DerivedDerivedClass+120 directly to overwrite them instead of going through the VTT

construction vtable for DerivedClass1-in-DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedClass1
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedClass1::VirtualFunction()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedClass1
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass1::VirtualFunction()
construction vtable for DerivedClass2-in-DerivedDerivedClass:
        .quad   16
        .quad   0
        .quad   typeinfo for DerivedClass2
        .quad   DerivedClass2::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -16
        .quad   0
        .quad   -16
        .quad   typeinfo for DerivedClass2
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedClass2::VirtualFunction()
vtable for DerivedDerivedClass:
        .quad   32
        .quad   0
        .quad   typeinfo for DerivedDerivedClass
        .quad   DerivedClass1::DerivedCommonFunction()
        .quad   DerivedDerivedClass::VirtualFunction()
        .quad   DerivedDerivedClass::DerivedDerivedCommonFunction()
        .quad   16
        .quad   -16
        .quad   typeinfo for DerivedDerivedClass
        .quad   non-virtual thunk to DerivedDerivedClass::VirtualFunction()
        .quad   DerivedClass2::DerivedCommonFunction2()
        .quad   -32
        .quad   0
        .quad   -32
        .quad   typeinfo for DerivedDerivedClass
        .quad   Base::CommonFunction()
        .quad   virtual thunk to DerivedDerivedClass::VirtualFunction()

virtual thunk to DerivedClass1::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK0
virtual thunk to DerivedClass2::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK1
virtual thunk to DerivedDerivedClass::VirtualFunction():
        mov     r10, QWORD PTR [rdi]
        add     rdi, QWORD PTR [r10-32]
        jmp     .LTHUNK2
non-virtual thunk to DerivedDerivedClass::VirtualFunction():
        sub     rdi, 16
        jmp     .LTHUNK3

        .set    .LTHUNK0,DerivedClass1::VirtualFunction()
        .set    .LTHUNK1,DerivedClass2::VirtualFunction()
        .set    .LTHUNK2,DerivedDerivedClass::VirtualFunction()
        .set    .LTHUNK3,DerivedDerivedClass::VirtualFunction()


每个继承的类都有自己的构造虚表和最派生的类,DerivedDerivedClass有一个虚表,每个子表都有一个子表,它使用指向子表的指针来覆盖继承类的构造函数为每个子对象存储的构造虚表指针。每个需要 thunk 的虚拟方法(虚拟 thunk 将对象指针从基址偏移到对象的开头,非虚拟 thunk 将对象指针从不是基对象的继承类对象偏移到对象的开头类型的整个对象DerivedDerivedClass)。这DerivedDerivedClass构造函数还使用虚拟表表 (VTT) 作为它需要使用的所有虚拟表指针的串行列表,并将其传递给每个构造函数(连同构造函数所针对的子对象地址),它们用来覆盖它们的和基地的 vtable 指针。

DerivedDerivedClass::DerivedDerivedClass()然后将 object+16 的地址和 VTT 的地址传递DerivedDerivedClass+24DerivedClass2::DerivedClass2()其程序集相同DerivedClass1::DerivedClass1()的行mov DWORD PTR [rax+8], 3,除了显然有 4 而不是 3 的行d = 4

在此之后,它将对象中的所有 3 个虚拟表指针替换为指向DerivedDerivedClass该类表示的 vtable 中的偏移量的指针。

调用d->VirtualFunction()in main

        mov     rax, QWORD PTR [rbp-24] //store pointer to object (and hence vtable pointer) in rax
        mov     rax, QWORD PTR [rax] //dereference this pointer to vtable pointer and store virtual table pointer in rax
        add     rax, 8 // add 8 to the pointer to get the 2nd function pointer in the table
        mov     rdx, QWORD PTR [rax] //dereference this pointer to get the address of the method to call
        mov     rax, QWORD PTR [rbp-24] //restore pointer to object in rax (-O0 is inefficient, yes)
        mov     rdi, rax  //pass object to the method
        call    rdx

d->DerivedCommonFunction();

        mov     rax, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax  //pass object to method
        call    rdx  //call the first function in the table

d->DerivedCommonFunction2();

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        lea     rdx, [rax+16]  //get the address of the 2nd subobject in the object
        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax+16] // get the vtable pointer of the 2nd subobject
        add     rax, 8  //call the 2nd function in this table
        mov     rax, QWORD PTR [rax]  //get the address of the 2nd function
        mov     rdi, rdx  //call it and pass the 2nd subobject to it
        call    rax

d->DerivedDerivedCommonFunction();

        mov     rax, QWORD PTR [rbp-24] //get the object pointer
        mov     rax, QWORD PTR [rax] //get the vtable pointer
        add     rax, 16 //get the 3rd function in the first virtual table (which is where virtual functions that that first appear in the most derived class go, because they belong to the full object which uses the virtual table pointer at the start of the object)
        mov     rdx, QWORD PTR [rax] //get the address of the object
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax  //call it and pass the whole object to it
        call    rdx

((DerivedClass2*)d)->DerivedCommonFunction2();

//it casts the object to its subobject and calls the corresponding method in its virtual table, which will be a non-virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L14
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 16
        jmp     .L15
.L14:
        mov     eax, 0
.L15:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L18
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, 16
        jmp     .L19
.L18:
        mov     edx, 0
.L19:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx

((Base*)d)->VirtualFunction();

//it casts the object to its subobject and calls the corresponding function in its virtual table, which will be a virtual thunk

        cmp     QWORD PTR [rbp-24], 0
        je      .L20
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        sub     rax, 24
        mov     rax, QWORD PTR [rax]
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-24]
        add     rax, rdx
        jmp     .L21
.L20:
        mov     eax, 0
.L21:
        cmp     QWORD PTR [rbp-24], 0
        cmp     QWORD PTR [rbp-24], 0
        je      .L24
        mov     rdx, QWORD PTR [rbp-24]
        mov     rdx, QWORD PTR [rdx]
        sub     rdx, 24
        mov     rdx, QWORD PTR [rdx]
        mov     rcx, rdx
        mov     rdx, QWORD PTR [rbp-24]
        add     rdx, rcx
        jmp     .L25
.L24:
        mov     edx, 0
.L25:
        mov     rdx, QWORD PTR [rdx]
        add     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        mov     rdi, rax
        call    rdx
于 2020-06-09T11:41:00.863 回答
1

这意味着对虚函数的调用将被转发到“正确的”类。

C++常见问题解答精简版FTW。

简而言之,它通常用于多继承场景中,其中形成了“钻石”层次结构。然后,当您在该类中调用函数并且该函数需要解析为该底部类之上的类 D1 或 D2 时,虚拟继承将打破在底部类中创建的歧义。有关图表和详细信息,请参阅常见问题解答项目

它也用于姐妹代表团,一个强大的功能(虽然不适合胆小的人)。请参阅常见问题解答。

另请参阅 Effective C++ 第 3 版中的第 40 条(第 2 版中的 43 条)。

于 2008-08-22T01:42:14.990 回答
1

你有点混乱。我不知道你是否混淆了一些概念。

您的 OP 中没有虚拟基类。你只有一个基类。

你做了虚拟继承。这通常用于多重继承,以便多个派生类使用基类的成员而不复制它们。

具有纯虚函数的基类不会被实例化。这需要 Paul 掌握的语法。它通常用于派生类必须定义这些函数。

我不想再解释这个了,因为我不完全明白你在问什么。

于 2008-08-22T01:48:04.977 回答
0

虚拟类虚拟继承不同。您无法实例化的虚拟类,虚拟继承完全是另一回事。

维基百科比我能更好地描述它。http://en.wikipedia.org/wiki/Virtual_inheritance

于 2008-08-22T01:52:04.920 回答