An in-depth exploration of C++ polymorphism ② – Inheritance relationships

Foreword

The previous chapter briefly described the calling link of virtual functions. This chapter mainly explores the polymorphic characteristics of class objects with various inheritance relationships in C++.

  • In-depth exploration of C++ polymorphism ① – Virtual function call link
  • A Deeper Exploration of C++ Polymorphism ② – Inheritance
  • A Deeper Exploration of C++ Polymorphism ③ – Virtual Destruction

1. Overview

Encapsulation, inheritance, and polymorphism are the three major features of C++, among which polymorphism is closely related to inheritance. The C++ language supports three inheritance relationships: single inheritance, multiple inheritance, and virtual inheritance:

Image source: “Polytype and Virtuality”

2. Inheritance relationship

2.1. Single inheritance

Single inheritance in C++ means that a class can only inherit properties and methods from one parent class.

Text source: ChatGPT

The single inheritance object class hierarchy of dynamic polymorphism is relatively simple:

  1. There is only one virtual pointer in the object memory, and it is at the first place.
  2. The virtual functions on the virtual table are covered layer by layer, and finally the virtual function table corresponding to the object is obtained. See the figure below for details.
  • Test the code.
/* g + + -O0 -std=c + + 11 -fdump-class-hierarchy test.cpp -o test */
#include <iostream>

class Base {<!-- -->
   public:
    virtual void vBaseFunc() {<!-- -->}
    virtual void vBaseFunc2() {<!-- -->}
    virtual void vBaseFunc3() {<!-- -->}

    long long m_base_data;
    long long m_base_data2;
};

class Base2 : public Base {<!-- -->
   public:
    virtual void vBaseFunc() {<!-- -->}
    virtual void vBase2Func() {<!-- --> std::cout << "Base2::vBase2Func" << std::endl; }
    virtual void vBase2Func2() {<!-- -->}

    long long m_base2_data;
    long long m_base2_data2;
};

class Derived : public Base2 {<!-- -->
   public:
    virtual void vBaseFunc2() {<!-- -->}
    virtual void vBase2Func() {<!-- --> std::cout << "Derived::vBase2Func" << std::endl; }
    virtual void vDerivedFunc() {<!-- -->}
    virtual void vDerivedFunc2() {<!-- -->}

    long long m_derived_data;
    long long m_derived_data2;
};
  • Class layout hierarchy.
Vtable for Base
# _ZTV4Base: vtable for Base
Base::_ZTV4Base: 5u entries
0 (int (*)(...))0
# _ZTI4Base: typeinfo for Base
8 (int (*)(...))( & amp; _ZTI4Base)
16 (int (*)(...))Base::vBaseFunc
24 (int (*)(...))Base::vBaseFunc2
32 (int (*)(...))Base::vBaseFunc3

Vtable for Base2
Base2::_ZTV5Base2: 7u entries
0 (int (*)(...))0
8 (int (*)(...))( & amp; _ZTI5Base2)
16 (int (*)(...))Base2::vBaseFunc
24 (int (*)(...))Base::vBaseFunc2
32 (int (*)(...))Base::vBaseFunc3
40 (int (*)(...))Base2::vBase2Func
48 (int (*)(...))Base2::vBase2Func2

# Derived virtual table.
Vtable for Derived
# _ZTV7Derived: vtable for Derived
Derived::_ZTV7Derived: 9u entries
0 (int (*)(...))0
# _ZTI7Derived: typeinfo for Derived
8 (int (*)(...))( & amp; _ZTI7Derived)
16 (int (*)(...))Base2::vBaseFunc
24 (int (*)(...))Derived::vBaseFunc2
32 (int (*)(...))Base::vBaseFunc3
40 (int (*)(...))Derived::vBase2Func
48 (int (*)(...))Base2::vBase2Func2
56 (int (*)(...))Derived::vDerivedFunc
64 (int (*)(...))Derived::vDerivedFunc2

#Inheritance relationship of classes
Class Derived
   size=56 align=8
   base size=56 base align=8
Derived (0x0x7fb058fa8478) 0
    # The virtual pointer points to the location of the virtual table.
    vptr=(( & amp; Derived::_ZTV7Derived) + 16u)
  Base2 (0x0x7fb058fa8a28) 0
      primary-for Derived (0x0x7fb058fa8478)
    Base (0x0x7fb058ee87e0) 0
        primary-for Base2 (0x0x7fb058fa8a28)
  • Virtual table integration.

  • The overall layout of the object.

  • Virtual function call.

    1. The first thing saved in the object is the virtual pointer vptr, which points to the virtual table.
    2. The virtual table address pointed to by the virtual pointer is offset 0x18 bytes to the high address, so that the Derived::vBase2Func virtual function address can be obtained and then called.
int main() {<!-- -->
    auto d = new Derived;
    std::cout << d << std::endl;

    auto b = static_cast<Base2 *>(d);
    std::cout << b << std::endl;
    b->vBase2Func();
    return 0;
}

// Output:
// 0x13a0010
// 0x13a0010
// Derived::vBase2Func

2.2. Multiple inheritance

C++ supports multiple inheritance, which means that a class can inherit properties and methods from multiple parent classes. In C++, multiple parent classes can be specified using comma separation.

Text source: ChatGPT.

  • Test the code.
/* g + + -O0 -std=c + + 11 -fdump-class-hierarchy test.cpp -o test */
#include <iostream>

class Base {<!-- -->
   public:
    virtual void vBaseFunc() {<!-- -->}
    virtual void vBaseFunc2() {<!-- -->}

    long long m_base_data;
    long long m_base_data2;
};

class Base2 {<!-- -->
   public:
    virtual void vBase2Func() {<!-- -->}
    virtual void vBase2Func2() {<!-- --> std::cout << "Base2::vBase2Func2" << std::endl; }

    long long m_base2_data;
    long long m_base2_data2;
};

class Base3 {<!-- -->
   public:
    virtual void vBase3Func() {<!-- -->}
    virtual void vBase3Func2() {<!-- -->}

    long long m_base3_data;
    long long m_base3_data2;
};

class Derived : public Base, public Base2, public Base3 {<!-- -->
   public:
    virtual void vBaseFunc() {<!-- -->}
    virtual void vBase2Func2() {<!-- --> std::cout << "Derived::vBase2Func2" << std::endl; }
    virtual void vBase3Func2() {<!-- -->}
    virtual void vDerivedFunc() {<!-- -->}
    virtual void vDerivedFunc2() {<!-- -->}

    long long m_derived_data;
    long long m_derived_data2;
};
  • Class layout hierarchy.
Vtable for Base
# _ZTV4Base: vtable for Base
Base::_ZTV4Base: 4u entries
0 (int (*)(...))0
# _ZTI4Base: typeinfo for Base
8 (int (*)(...))( & amp; _ZTI4Base)
16 (int (*)(...))Base::vBaseFunc
24 (int (*)(...))Base::vBaseFunc2

Vtable for Base2
# _ZTV5Base2: vtable for Base2
Base2::_ZTV5Base2: 4u entries
0 (int (*)(...))0
# _ZTI5Base2: typeinfo for Base2
8 (int (*)(...))( & amp; _ZTI5Base2)
16 (int (*)(...))Base2::vBase2Func
24 (int (*)(...))Base2::vBase2Func2

Vtable for Base3
# _ZTV5Base3: vtable for Base3
Base3::_ZTV5Base3: 4u entries
0 (int (*)(...))0
# _ZTI5Base3: typeinfo for Base3
8 (int (*)(...))( & amp; _ZTI5Base3)
16 (int (*)(...))Base3::vBase3Func
24 (int (*)(...))Base3::vBase3Func2

Vtable for Derived
# _ZTV7Derived: vtable for Derived
Derived::_ZTV7Derived: 16u entries
0 (int (*)(...))0
# _ZTI7Derived: typeinfo for Derived
8 (int (*)(...))( & amp; _ZTI7Derived)
16 (int (*)(...))Derived::vBaseFunc
24 (int (*)(...))Base::vBaseFunc2
32 (int (*)(...))Derived::vBase2Func2
40 (int (*)(...))Derived::vBase3Func2
48 (int (*)(...))Derived::vDerivedFunc
56 (int (*)(...))Derived::vDerivedFunc2
64 (int (*)(...))-24
72 (int (*)(...))( & amp; _ZTI7Derived)
80 (int (*)(...))Base2::vBase2Func
# _ZThn24_N7Derived11vBase2Func2Ev: non-virtual thunk to Derived::vBase2Func2()
88 (int (*)(...))Derived::_ZThn24_N7Derived11vBase2Func2Ev
96 (int (*)(...))-48
104 (int (*)(...))( & amp; _ZTI7Derived)
112 (int (*)(...))Base3::vBase3Func
# _ZThn48_N7Derived11vBase3Func2Ev: non-virtual thunk to Derived::vBase3Func2()
120 (int (*)(...))Derived::_ZThn48_N7Derived11vBase3Func2Ev

Class Derived
   size=88 align=8
   base size=88 base align=8
Derived (0x0x7f4196042348) 0
    vptr=(( & amp; Derived::_ZTV7Derived) + 16u)
  Base (0x0x7f4195f3e840) 0
      primary-for Derived (0x0x7f4196042348)
  Base2 (0x0x7f4195f3e8a0) 24
      vptr=(( & amp; Derived::_ZTV7Derived) + 80u)
  Base3 (0x0x7f4195f3e900) 48
      vptr=(( & amp; Derived::_ZTV7Derived) + 112u)
  • Virtual table integration.

    1. First, the virtual table of the derived class is combined with the virtual table of the first base class to form a virtual table unit, and the virtual function of the base class is overridden.
    2. Other base classes serve as an independent virtual table unit. When a derived class virtual function overrides a base class virtual function, the base class corresponds to the virtual function and jumps to the corresponding virtual function of the first virtual table unit through thunk technology.

  • The overall layout of the object. As can be seen from the picture below:

    1. Multiple inheritance has multiple virtual pointers and points to corresponding virtual table units.
    2. If a derived class has N multiple inheritance from a single base class, then its objects have N virtual pointers and virtual table locations.

  • Virtual function call. With the above understanding of memory layout, it should not be difficult for us to understand how the following base class pointer calls the derived class virtual function:
int main() {<!-- -->
    auto d = new Derived;
    std::cout << d << std::endl;

    auto b = static_cast<Base2 *>(d);
    std::cout << b << std::endl;
    b->vBase2Func2();
    return 0;
}

// Output:
// 0x13db010
// 0x13db028
// Derived::vBase2Func2
  1. The Base2 pointer points to the address where vptr2 is stored: offset 0x18 bytes from the top of object memory to the high address to obtain the vptr2 virtual pointer.
  2. The virtual table address pointed by the vptr2 pointer is offset 0x8 bytes to the high address, and the non-virtual thunk to Derived::vBase2Func2() address is obtained.
  3. Jump to the Derived::vBase2Func2 virtual function through the non-virtual thunk to Derived::vBase2Func2() address, and obtain the corresponding virtual function address on the virtual table for calling.

  • Understand how the function thunk to jump works through assembly.
# thunk to jump principle (assembly).
0000000000400aba <non-virtual thunk to Derived::vBase2Func2()>:
  # The rdi register stores the address pointed to by the b pointer, which is offset by 0x18 bytes toward the lower address.
  # That is, the rdi register stores the first address of Derived memory.
  # In other words, pass Derived's this pointer as a parameter to the Derived::vBase2Func2 function.
  400aba: 48 83 ef 18 sub $0x18,%rdi
  # Call the Derived::vBase2Func2() function.
  400abe: eb d0 jmp 400a90 <Derived::vBase2Func2()>
  • Think about it, is the polymorphic instance object of multiple inheritance above released in the following way correct? ! (For details, please refer to: Virtual Destruction).
int main() {<!-- -->
    Base2* b = new Derived;
    delete b;
    return 0;
}

2.3. Virtual inheritance

Multiple inheritance allows a class to have the characteristics of multiple different parent classes, but it may also cause some problems, such as the diamond inheritance problem. To solve this problem, C++ provides the concepts of virtual inheritance and virtual base classes. Virtual inheritance solves the diamond inheritance problem by ensuring that there is only one instance of a shared base class.

In C++, virtual inheritance is a special type of inheritance. It is used to solve the diamond inheritance problem in multiple inheritance. When a class inherits from multiple base classes through virtual inheritance, only one instance of the base class is retained and inheritance is not repeated. This avoids the ambiguity and redundancy caused by diamond inheritance. In virtual inheritance, the derived class needs to declare the base class using the keyword “virtual”.

Text source: ChatGPT

Because there is a shared base class in the inheritance relationship, in order to avoid the shared base class generating multiple object copies and wasting memory, the memory layout of virtual inheritance will also be different from single inheritance and multiple inheritance:

  1. The member data of the public base class is stored at the bottom of the object memory.
  2. Virtual inheritance introduces VTT (Virtual Table Table) to construct a virtual table.
  3. The virtual table prefix introduces the vbase_offset offset: the memory location offset between the current virtual table and the common base class virtual table.

The class hierarchy structure of virtual inheritance is a bit complicated. Interested friends can refer to: What is the VTT for a class.

2.3.1. Overall layout of objects
  • Test the code.
/* g + + -O0 -std=c + + 11 -fdump-class-hierarchy test.cpp -o test */
#include <iostream>

class Base {<!-- -->
   public:
    virtual void vBaseFunc() {<!-- -->}
    virtual void vBaseFunc2() {<!-- -->}

    long long m_base_data = 0x11;
    long long m_base_data2 = 0x12;
};

class Base2 : virtual public Base {<!-- -->
   public:
    virtual void vBaseFunc() {<!-- -->}
    virtual void vBase2Func() {<!-- -->}
    virtual void vBase2Func2() {<!-- -->}

    long long m_base2_data = 0x21;
    long long m_base2_data2 = 0x22;
};

class Base3 : virtual public Base {<!-- -->
   public:
    virtual void vBaseFunc2() {<!-- -->}
    virtual void vBase3Func() {<!-- -->}
    virtual void vBase3Func2() {<!-- --> std::cout << "Base3::vBase3Func2" << std::endl; }
    long long m_base3_data = 0x31;
    long long m_base3_data2 = 0x32;
};

class Derived : public Base2, public Base3 {<!-- -->
   public:
    virtual void vBase2Func() {<!-- -->}
    virtual void vBase3Func2() {<!-- --> std::cout << "Derived::vBase3Func2" << std::endl; }
    virtual void vDerivedFunc() {<!-- -->}
    virtual void vDerivedFunc2() {<!-- -->}

    long long m_derived_data = 0x41;
    long long m_derived_data2 = 0x42;
};
  • Class object memory layout.

Vtable for Derived
# _ZTV7Derived: vtable for Derived
Derived::_ZTV7Derived: 21u entries
0 64u
8 (int (*)(...))0
# _ZTI7Derived: typeinfo for Derived
16 (int (*)(...))( & amp; _ZTI7Derived)
24 (int (*)(...))Base2::vBaseFunc
32 (int (*)(...))Derived::vBase2Func
40 (int (*)(...))Base2::vBase2Func2
48 (int (*)(...))Derived::vBase3Func2
56 (int (*)(...))Derived::vDerivedFunc
64 (int (*)(...))Derived::vDerivedFunc2
72 40u
80 (int (*)(...))-24
88 (int (*)(...))( & amp; _ZTI7Derived)
96 (int (*)(...))Base3::vBaseFunc2
104 (int (*)(...))Base3::vBase3Func
# _ZThn24_N7Derived11vBase3Func2Ev: non-virtual thunk to Derived::vBase3Func2()
112 (int (*)(...))Derived::_ZThn24_N7Derived11vBase3Func2Ev
120 18446744073709551576u # -40
128 18446744073709551552u # -64
136 (int (*)(...))-64
144 (int (*)(...))( & amp; _ZTI7Derived)
# _ZTv0_n24_N5Base29vBaseFuncEv: virtual thunk to Base2::vBaseFunc()
152 (int (*)(...))Base2::_ZTv0_n24_N5Base29vBaseFuncEv
# _ZTv0_n32_N5Base310vBaseFunc2Ev: virtual thunk to Base3::vBaseFunc2()
160 (int (*)(...))Base3::_ZTv0_n32_N5Base310vBaseFunc2Ev

Construction vtable for Base2 (0x0x7fd19d6aea90 instance) in Derived
# _ZTC7Derived0_5Base2: construction vtable for Base2-in-Derived
Derived::_ZTC7Derived0_5Base2: 12u entries
0 64u
8 (int (*)(...))0
# _ZTI5Base2: typeinfo for Base2
16 (int (*)(...))( & amp; _ZTI5Base2)
24 (int (*)(...))Base2::vBaseFunc
32 (int (*)(...))Base2::vBase2Func
40 (int (*)(...))Base2::vBase2Func2
48 0u
56 18446744073709551552u # -64
64 (int (*)(...))-64
# _ZTI5Base2: typeinfo for Base2
72 (int (*)(...))( & amp; _ZTI5Base2)
# _ZTv0_n24_N5Base29vBaseFuncEv: virtual thunk to Base2::vBaseFunc()
80 (int (*)(...))Base2::_ZTv0_n24_N5Base29vBaseFuncEv
88 (int (*)(...))Base::vBaseFunc2

Construction vtable for Base3 (0x0x7fd19d6aeaf8 instance) in Derived
Derived::_ZTC7Derived24_5Base3: 12u entries
0 40u
8 (int (*)(...))0
# _ZTI5Base3: typeinfo for Base3
16 (int (*)(...))( & amp; _ZTI5Base3)
24 (int (*)(...))Base3::vBaseFunc2
32 (int (*)(...))Base3::vBase3Func
40 (int (*)(...))Base3::vBase3Func2
48 18446744073709551576u # -40
56 0u
64 (int (*)(...))-40
# _ZTI5Base3: typeinfo for Base3
72 (int (*)(...))( & amp; _ZTI5Base3)
80 (int (*)(...))Base::vBaseFunc
# _ZTv0_n32_N5Base310vBaseFunc2Ev: virtual thunk to Base3::vBaseFunc2()
88 (int (*)(...))Base3::_ZTv0_n32_N5Base310vBaseFunc2Ev

VTT for Derived
# _ZTV7Derived: vtable for Derived
Derived::_ZTT7Derived: 7u entries
0 (( & amp; Derived::_ZTV7Derived) + 24u)
# _ZTC7Derived0_5Base2: construction vtable for Base2-in-Derived
8 (( & amp; Derived::_ZTC7Derived0_5Base2) + 24u)
16 (( & amp; Derived::_ZTC7Derived0_5Base2) + 80u)
# _ZTC7Derived24_5Base3: construction vtable for Base3-in-Derived
24 (( & amp; Derived::_ZTC7Derived24_5Base3) + 24u)
32 (( & amp; Derived::_ZTC7Derived24_5Base3) + 80u)
40 (( & amp; Derived::_ZTV7Derived) + 152u)
48 (( & amp; Derived::_ZTV7Derived) + 96u)

Class Derived
   size=88 align=8
   base size=64 base align=8
Derived (0x0x7fd19d7401c0) 0
    vptridx=0u vptr=(( & amp; Derived::_ZTV7Derived) + 24u)
  Base2 (0x0x7fd19d6aea90) 0
      primary-for Derived (0x0x7fd19d7401c0)
      subvttidx=8u
    Base (0x0x7fd19d5ee840) 64 virtual
        vptridx=40u vbaseoffset=-24 vptr=(( & amp; Derived::_ZTV7Derived) + 152u)
  Base3 (0x0x7fd19d6aeaf8) 24
      subvttidx=24u vptridx=48u vptr=(( & amp; Derived::_ZTV7Derived) + 96u)
    Base (0x0x7fd19d5ee840) alternative-path
2.3.2. Construction sequence

We can understand through the construction sequence of the class: how the object memory layout is constructed step by step. When constructing the derived class Derived, construct the base class first, and then construct itself after the base class is constructed.

  • Construction process.
|-- main
    |-- ...
    |-- Derived::Derived()
        |-- Base::Base()
        |-- Base2::Base2()
        |-- Base3::Base3()
  • Construction process (assembly).
...
0x400b33: e8 34 02 00 00 callq 0x400d6c <Derived::Derived()>
...
0x400d83: e8 06 ff ff ff callq 400c8e <Base::Base()>
...
0x400d97: e8 20 ff ff ff callq 400cbc <Base2::Base2()>
...
0x400daf: e8 60 ff ff ff callq 400d14 <Base3::Base3()>
  • Construct Base.

Vtable for Base
# _ZTV4Base: vtable for Base
Base::_ZTV4Base: 4u entries
0 (int (*)(...))0
# _ZTI4Base: typeinfo for Base
8 (int (*)(...))( & amp; _ZTI4Base)
16 (int (*)(...))Base::vBaseFunc
24 (int (*)(...))Base::vBaseFunc2

Class Base
   size=24 align=8
   base size=24 base align=8
Base (0x0x7fd19d5ee720) 0
    vptr=(( & amp; Base::_ZTV4Base) + 16u)
  • Construct Base2.

Construction vtable for Base2 (0x0x7fd19d6aea90 instance) in Derived
# _ZTC7Derived0_5Base2: construction vtable for Base2-in-Derived
Derived::_ZTC7Derived0_5Base2: 12u entries
0 64u
8 (int (*)(...))0
# _ZTI5Base2: typeinfo for Base2
16 (int (*)(...))( & amp; _ZTI5Base2)
24 (int (*)(...))Base2::vBaseFunc
32 (int (*)(...))Base2::vBase2Func
40 (int (*)(...))Base2::vBase2Func2
48 0u
56 18446744073709551552u # -64
64 (int (*)(...))-64
# _ZTI5Base2: typeinfo for Base2
72 (int (*)(...))( & amp; _ZTI5Base2)
# _ZTv0_n24_N5Base29vBaseFuncEv: virtual thunk to Base2::vBaseFunc()
80 (int (*)(...))Base2::_ZTv0_n24_N5Base29vBaseFuncEv
88 (int (*)(...))Base::vBaseFunc2

VTT for Derived
# _ZTV7Derived: vtable for Derived
Derived::_ZTT7Derived: 7u entries
0 (( & amp; Derived::_ZTV7Derived) + 24u)
# _ZTC7Derived0_5Base2: construction vtable for Base2-in-Derived
8 (( & amp; Derived::_ZTC7Derived0_5Base2) + 24u)
16 (( & amp; Derived::_ZTC7Derived0_5Base2) + 80u)
# _ZTC7Derived24_5Base3: construction vtable for Base3-in-Derived
24 (( & amp; Derived::_ZTC7Derived24_5Base3) + 24u)
32 (( & amp; Derived::_ZTC7Derived24_5Base3) + 80u)
40 (( & amp; Derived::_ZTV7Derived) + 152u)
48 (( & amp; Derived::_ZTV7Derived) + 96u)
  • Construct Base3.

VTT for Derived
# _ZTV7Derived: vtable for Derived
Derived::_ZTT7Derived: 7u entries
0 (( & amp; Derived::_ZTV7Derived) + 24u)
# _ZTC7Derived0_5Base2: construction vtable for Base2-in-Derived
8 (( & amp; Derived::_ZTC7Derived0_5Base2) + 24u)
16 (( & amp; Derived::_ZTC7Derived0_5Base2) + 80u)
# _ZTC7Derived24_5Base3: construction vtable for Base3-in-Derived
24 (( & amp; Derived::_ZTC7Derived24_5Base3) + 24u)
32 (( & amp; Derived::_ZTC7Derived24_5Base3) + 80u)
40 (( & amp; Derived::_ZTV7Derived) + 152u)
48 (( & amp; Derived::_ZTV7Derived) + 96u)

Construction vtable for Base3 (0x0x7fd19d6aeaf8 instance) in Derived
# _ZTC7Derived24_5Base3: construction vtable for Base3-in-Derived
Derived::_ZTC7Derived24_5Base3: 12u entries
0 40u
8 (int (*)(...))0
# _ZTI5Base3: typeinfo for Base3
16 (int (*)(...))( & amp; _ZTI5Base3)
24 (int (*)(...))Base3::vBaseFunc2
32 (int (*)(...))Base3::vBase3Func
40 (int (*)(...))Base3::vBase3Func2
48 18446744073709551576u # -40
56 0u
64 (int (*)(...))-40
# _ZTI5Base3: typeinfo for Base3
72 (int (*)(...))( & amp; _ZTI5Base3)
80 (int (*)(...))Base::vBaseFunc
# _ZTv0_n32_N5Base310vBaseFunc2Ev: virtual thunk to Base3::vBaseFunc2()
88 (int (*)(...))Base3::_ZTv0_n32_N5Base310vBaseFunc2Ev
  • Construct Derived (see Overall Layout above).

2.3.3. Virtual function call
int main() {<!-- -->
    auto d = new Derived;
    std::cout << d << std::endl;

    auto b = static_cast<Base3 *>(d);
    std::cout << b << std::endl;
    b->vBase3Func2();
    return 0;
}

// Output:
// 0x9fa010
// 0x9fa028
// Derived::vBase3Func2
  1. b The pointer points to the address where vptr.base3 is stored: offset 0x18 bytes from the top of Derived object memory.
  2. The virtual table address pointed to by the vptr.base3 pointer is offset 0x10 bytes to the high address, and the non-virtual thunk to Derived::vBase3Func2() function address is obtained.
  3. Jump to the Derived::vBase3Func2 virtual function through the non-virtual thunk to Derived::vBase3Func2() address, and obtain the corresponding virtual function on the virtual table for calling.

3. Postscript

  • To understand the polymorphic object memory layout, you must pay attention to understanding how (multiple) virtual pointers are offset according to different base class pointers. When the virtual pointer points to the virtual table, to obtain the corresponding virtual function, the virtual pointer must be offset. Only by moving a certain position can the virtual function on the corresponding virtual table be located.

  • If you want to use one word to describe polymorphism, it would be overwriting. Derived classes override base class virtual functions. Like layers, the upper layer (derived class) covers the lower layer (base class), layer by layer. , and finally got the overwritten result; this is also the core way of thinking for us to understand the virtual table structure.

  • Regarding C++ polymorphic exploration with inheritance relationships, due to my limited level, the above only made some basic and simple Demo analysis, and some application scenarios were not covered (such as virtual destructor).

  • Many technical details are not mentioned in the article. Interested friends can write the demo and debug it with gdb, check the address data on the object memory layout, and disassemble to check whether the logic of the object construction is consistent with their own understanding. In this way Only in this way can we find the essence of the answer in the ever-changing appearance of the problem.