Что такое виртуальный базовый класс в C++?

Я хочу знать, что такое "виртуальный базовый класс" и что он означает.

Позвольте мне показать пример:

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

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

10 ответов

Решение

Виртуальные базовые классы, используемые в виртуальном наследовании, являются способом предотвращения появления нескольких "экземпляров" данного класса в иерархии наследования при использовании множественного наследования.

Рассмотрим следующий сценарий:

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

Приведенная выше иерархия классов приводит к "страшному алмазу", который выглядит следующим образом:

  A
 / \
B   C
 \ /
  D

Экземпляр D будет состоять из B, который включает в себя A, и C, который также включает в себя A. Таким образом, у вас есть два "экземпляра" (для лучшего выражения) из 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

Надеюсь, что это поможет в качестве мини-резюме. Для получения дополнительной информации, прочитайте это и это. Хороший пример также доступен здесь.

О расположении памяти

Напомним, что проблема с Dreaded Diamond заключается в том, что базовый класс присутствует несколько раз. Таким образом, при регулярном наследовании вы считаете, что имеете:

  A
 / \
B   C
 \ /
  D

Но в макете памяти у вас есть:

A   A
|   |
B   C
 \ /
  D

Это объясняет почему когда звоните D::foo()У вас есть проблема неоднозначности. Но настоящая проблема возникает, когда вы хотите использовать переменную-член A, Например, скажем, у нас есть:

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

Когда вы попытаетесь получить доступ m_iValue от Dкомпилятор будет протестовать, потому что в иерархии он увидит два m_iValue, Не один. И если вы измените один, скажем, B::m_iValue (это A::m_iValue родитель B), C::m_iValue не будет изменено (это A::m_iValue родитель C).

Вот где виртуальное наследование пригодится, так как с его помощью вы вернетесь к истинному алмазному макету, а не только одному foo() только метод, но также один и только один m_iValue,

Что может пойти не так?

Представить:

  • A имеет некоторые основные функции.
  • B добавляет к этому какой-то классный массив данных (например)
  • C добавляет к этому некоторые интересные функции, такие как шаблон наблюдателя (например, на m_iValue).
  • D наследуется от B а также Cи, следовательно, из A,

С нормальным наследованием, модифицирующим m_iValue от D является неоднозначным, и это должно быть решено. Даже если это так, есть два m_iValues внутри D, так что лучше запомните это и обновите оба одновременно.

С виртуальным наследованием, изменение m_iValue от D все в порядке... но... допустим, что у вас есть D, Через его C интерфейс, вы прикрепили наблюдателя. И через его B Интерфейс, вы обновляете классный массив, который имеет побочный эффект прямого изменения m_iValue...

Как смена m_iValue делается напрямую (без использования метода виртуального метода доступа), наблюдатель "прослушивает" через C не будет вызван, потому что код, реализующий прослушивание, находится в C, а также B не знает об этом...

Заключение

Если у вас есть бриллиант в вашей иерархии, это означает, что у вас есть 95%, чтобы сделать что-то не так с указанной иерархией.

Для объяснения множественного наследования с помощью виртуальных баз требуется знание объектной модели C++. И объяснение темы лучше всего делать в статье, а не в поле для комментариев.

Наилучшим понятным объяснением, которое я нашел и которое разрешило все мои сомнения по этому вопросу, была эта статья: https://web.archive.org/web/20160413064252/http://www.phpcompiler.org/articles/virtualinheritance.html.

Вам действительно не нужно будет читать что-либо еще по этой теме (если вы не писатель компилятора) после прочтения этого...

Виртуальный базовый класс - это класс, создание которого невозможно, из него нельзя создать прямой объект.

Я думаю, что вы путаете две совершенно разные вещи. Виртуальное наследование - это не то же самое, что абстрактный класс. Виртуальное наследование изменяет поведение вызовов функций; иногда он разрешает вызовы функций, которые в противном случае были бы неоднозначными, иногда он откладывает обработку вызовов функций на класс, отличный от того, который можно ожидать в невиртуальном наследовании.

Я хотел бы добавить к добрым разъяснениям OJ.

Виртуальное наследство не обходится без цены. Как и со всеми виртуальными вещами, вы получаете удар по производительности. Этот хит производительности может быть менее элегантным.

Вместо того, чтобы разбивать алмаз путем виртуального извлечения, вы можете добавить еще один слой к алмазу, чтобы получить что-то вроде этого:

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

Ни один из классов не наследует виртуально, все наследуют публично. Классы D21 и D22 будут затем скрывать виртуальную функцию f (), которая неоднозначна для DD, возможно, объявив функцию частной. Каждый из них определил бы функцию-оболочку, f1 () и f2() соответственно, каждый из которых вызывал бы локальный класс (private) f (), таким образом разрешая конфликты. Класс DD вызывает f1 (), если он хочет D11::f() и f2(), если он хочет D12::f(). Если вы определите встроенные оболочки, вы, вероятно, получите около нуля накладных расходов.

Конечно, если вы можете изменить D11 и D12, вы можете сделать один и тот же трюк внутри этих классов, но часто это не так.

В дополнение к тому, что уже было сказано о множественном и виртуальном наследовании, в журнале д-ра Добба есть очень интересная статья: множественное наследование считается полезным

Пример использования бриллиантового наследования

В этом примере показано, как использовать виртуальный базовый класс в типичном сценарии: для решения проблемы наследования алмазов.

#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);
}

Обычное наследование

При типичном трехуровневом наследовании не-ромбовидного невиртуального наследования, когда вы создаете экземпляр нового наиболее производного объекта, вызывается new, и размер, требуемый для объекта, определяется компилятором из типа класса и передается в new.

новый имеет подпись:

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

И звонит malloc, возвращая указатель void

Затем он передается конструктору самого производного объекта, который немедленно вызовет средний конструктор, а затем средний конструктор немедленно вызовет базовый конструктор. Затем база сохраняет указатель на свою виртуальную таблицу в начале объекта, а затем его атрибуты после него. Затем он возвращается к среднему конструктору, который сохранит указатель виртуальной таблицы в том же месте, а затем свои атрибуты после атрибутов, которые были бы сохранены базовым конструктором. Он возвращается к наиболее производному конструктору, который сохраняет указатель на свою виртуальную таблицу в том же месте, а затем его атрибуты после атрибутов, которые были бы сохранены средним конструктором.

Поскольку указатель виртуальной таблицы перезаписывается, указатель виртуальной таблицы всегда оказывается одним из наиболее производных классов. Виртуальность распространяется в сторону самого производного класса, поэтому, если функция является виртуальной в среднем классе, она будет виртуальной в самом производном классе, но не в базовом классе. Если вы полиморфно приведете экземпляр наиболее производного класса к указателю на базовый класс, то компилятор не разрешит это косвенным вызовом виртуальной таблицы, а вместо этого вызовет функцию напрямую.A::function(). Если функция является виртуальной для того типа, к которому вы ее применили, она разрешит вызов виртуальной таблицы, которая всегда будет таковой из наиболее производного класса. Если он не виртуальный для этого типа, он просто вызоветType::function() и передайте ему указатель объекта, приведя его к типу.

Actually when I say pointer to its virtual table, it's actually always an offset of 16 into the virtual table.

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 is not required again in more-derived classes if it is virtual in a less-derived class because it propagates. But it can be used to show that the function is indeed a virtual function, without having to check the classes it inherits's type definitions.

override is another compiler guard that says that this function is overriding something and if it isn't then throw a compiler error.

= 0 means that this is an abstract function

final prevents a virtual function from being implemented again in a more derived class and will make sure that the virtual table of the most derived class contains the final function of that class.

= default makes it explicit in documentation that the compiler will use the default implementation

= delete give a compiler error if a call to this is attempted

Virtual Inheritance

Consider

class Base
  {
      int a = 1;
      int b = 2;
  public:
      void virtual CommonFunction(){} ;
      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();
 }

Without virtually inheriting the bass class you will get an object that looks like this:

Instead of this:

I.e. there will be 2 base objects.

In the virtual diamond inheritance situation above, after new is called, it calls the most derived constructor and in that constructor, it calls all 3 derived constructors passing offsets into its virtual table table, instead of calling just calling DerivedClass1::DerivedClass1() and DerivedClass2::DerivedClass2() and then those both calling Base::Base()

The following is all compiled in debug mode -O0 so there will be redundant assembly

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 d
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
        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 object 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
.LBE5:
        nop
        leave
        ret

It calls Base::Base() with a pointer to the object offset 32. Base stores a pointer to its virtual table at the address it receives and its members after it.

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() then calls DerivedClass1::DerivedClass1() with a pointer to the object offset 0 and also passes the address of 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]   //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
        .quad   construction vtable for DerivedClass1-in-DerivedDerivedClass+72
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+24
        .quad   construction vtable for DerivedClass2-in-DerivedDerivedClass+72
        .quad   vtable for DerivedDerivedClass+120
        .quad   vtable for DerivedDerivedClass+72

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::DerivedDerivedClass() then passes the address of the object+16 and the address of VTT for DerivedDerivedClass+24 to DerivedClass2::DerivedClass2() whose assembly is identical to DerivedClass1::DerivedClass1() except for the line mov DWORD PTR [rax+8], 3 which obviously has a 4 instead of 3 for d = 4.

After this, it replaces all 3 virtual table pointers in the object with pointers to offsets in DerivedDerivedClass's vtable to the representation for that class.

d->VirtualFunction();:

        mov     rax, QWORD PTR [rbp-24] //store pointer to virtual table in rax 
        mov     rax, QWORD PTR [rax] //dereference and store in rax
        add     rax, 8 // call the 2nd function in the table
        mov     rdx, QWORD PTR [rax] //dereference 
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        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
        call    rdx

d->DerivedCommonFunction2();:

        mov     rax, QWORD PTR [rbp-24]
        lea     rdx, [rax+16]
        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax+16]
        add     rax, 8
        mov     rax, QWORD PTR [rax]
        mov     rdi, rdx
        call    rax

d->DerivedDerivedCommonFunction();:

        mov     rax, QWORD PTR [rbp-24]
        mov     rax, QWORD PTR [rax]
        add     rax, 16
        mov     rdx, QWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    rdx

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

        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();:

        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

Это означает, что вызов виртуальной функции будет перенаправлен в "правильный" класс.

C++ FAQ Lite FTW.

Короче говоря, он часто используется в сценариях множественного наследования, где формируется "алмазная" иерархия. Виртуальное наследование разрушит неоднозначность, созданную в нижнем классе, когда вы вызываете функцию в этом классе, и функция должна быть преобразована в класс D1 или D2 выше этого нижнего класса. Смотрите пункт FAQ для диаграммы и деталей.

Это также используется в делегировании сестры, мощная функция (хотя и не для слабонервных). Смотрите этот FAQ.

Также см. Пункт 40 в Effective C++, 3-е издание (43 во 2-м издании).

Ты немного сбиваешь с толку. Я не знаю, смешиваете ли вы некоторые понятия.

У вас нет виртуального базового класса в вашем OP. У вас просто есть базовый класс.

Вы сделали виртуальное наследование. Это обычно используется в множественном наследовании, так что несколько производных классов используют члены базового класса, не воспроизводя их.

Базовый класс с чисто виртуальной функцией не создается. для этого требуется синтаксис, который использует Пол. Обычно он используется для того, чтобы производные классы определяли эти функции.

Я не хочу больше об этом объяснять, потому что я не совсем понимаю, что вы спрашиваете.

Виртуальные классы - это не виртуальное наследование. Виртуальные классы, которые вы не можете создать, виртуальное наследование - это совсем другое.

Википедия описывает это лучше, чем я. http://en.wikipedia.org/wiki/Virtual_inheritance

Другие вопросы по тегам