In a strict sense, the answer to "how does inheritance work at runtime?" is "however the compiler-writer designed it". I.e., the language specification only describes the behavior to be achieved, not the mechanism to achieve it.
In that light, the following should be seen as analogy. Compilers will do something analogous to the following:
Given a class Base
:
class Base
{
int a;
int b;
public:
Base()
: a(5),
b(3)
{ }
virtual void foo() {}
virtual void bar() {}
};
The compiler will define two structures: one we'll call the "storage layout" -- this defines the relative locations of member variables and other book-keeping info for an object of the class; the second structure is the "virtual dispatch table" (or vtable). This is a structure of pointers to the implementations of the virtual methods for the class.
This figure gives an object of type Base

Now lets look as the equivalent structure for a derived class, Derived
:
class Derived : public Base
{
int c;
public:
Derived()
: Base(),
c(4)
{ }
virtual void bar() //Override
{
c = a*5 + b*3;
}
};
For an object of type Derived
, we have a similar structure:

The important observation is that the in-memory representation of both the member-variable storage and the vtable entries, for members a
and b
, and methods foo
and bar
, are identical between the base class and subclass. So a pointer of type Base *
that happens to point to an object of type Derived
will still implement an access to the variable a
as a reference to the first storage offset after the vtable pointer. Likewise, calling ptr->bar()
passes control to the method in the second slot of the vtable. If the object is of type Base
, this is Base::bar()
; if the object is of type Derived
, this is Derived::bar()
.
In this analogy, the this
pointer points to the member storage block. Hence, the implementation of Derived::bar()
can access the member variable c
by fetching the 3rd storage slot after the vtable pointer, relative to this
. Note that this storage slot exists whenever Derived::bar()
sits in the second vtable slot...i.e., when the object really is of type Derived
.
A brief aside on the debugging insanity that can arise from corrupting the vtable pointer for compilers that use a literal vtable pointer at offset 0 from this
:
#include <iostream>
class A
{
public:
virtual void foo()
{
std::cout << "A::foo()" << std::endl;
}
};
class B
{
public:
virtual void bar()
{
std::cout << "B::bar()" << std::endl;
}
};
int main(int argc, char *argv[])
{
A *a = new A();
B *b = new B();
std::cout << "A: ";
a->foo();
std::cout << "B: ";
b->bar();
//Frankenobject
*((void **)a) = *((void **)b); //Overwrite a's vtable ptr with b's.
std::cout << "Franken-AB: ";
a->foo();
}
Yields:
$ ./a.out
A: A::foo()
B: B::bar()
Franken-AB: B::bar()
$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609
...note the lack of an inheritance relationship between A
and B
... :scream: