class Foo
{
int i;
int j;
double y;
};
In this case we need at least 16 bytes, because each int takes 4
bytes and the double takes 8. The bytes for the fields are
typically laid out
sequentially, in the order fields appear textually in the class
definition. So for Foo, the first four bytes are i, the next four are
j, and the last eight are y.

class Foo Offset Example code As the compiler sees it: pointer to f + offset, cast to the proper type
{ Foo f; note that "unsigned char*" means pointer to a raw byte
int i; 0 f.i = 7; *(int*)((unsigned char*)&f + 0) = 7;
int j; 4 f.j = 42; *(int*)((unsigned char*)&f + 4) = 42;
double y; 8 f.y = 0.357; *(double*)((unsigned char*)&f + 8) = 0.357;
};
If you don't believe me, you can try this little experiment.
Compile and run the code below, and you will see that the fields
are assigned their proper values using the "pointer to f +
offset, cast to the proper type" method:
| ex1.cpp | running |
$ g++ ex1.cpp $ ./a.out 7 42 0.357 |
| ex1a.cpp | running | discussion |
$ g++ ex1a.cpp $ ./a.out sizeof(f) = 16 0x7ffc208ee160 f 0x7ffc208ee160 f.i 0x7ffc208ee168 f.y |
|

class Foo
{
public:
int i;
int j;
double y;
int bar() { i++; return j; }
};
... where there is no inheritance and thus no polymorphism to
worry about. The method "bar" is different from normal
functions because it has access to the fields of the object on
which it was called. For example, if we have Foo f and call
f.bar(), the "bar" function will access f's i and j fields. We
handle this by rewriting methods to add a pointer to the object
on which the method was called as the first parameter to the
function. In the above example, we would rewrite bar as:
int bar() { i++; return j; }
would be rewritten like this:
int bar(Foo* this) {
*(int*)((unsigned char*)this + 0)++;
return *(int*)((unsigned char*)this + 4);
}
Here's a sligthly prettier version of the same thing:
int bar(Foo* this) {
unsigned char* ptr = (unsigned char*)this;
int* iptr = (int*)(ptr + 0); // offset 0
int* jptr = (int*)(ptr + 4); // offset 4
*iptr++;
return *jptr;
}
int bar(Foo* this) { this->i++; return this->j; }
Additionally, each method call would have to be rewritten as
well, so that for example:
f.bar() is rewritten as bar(f)In this way methods (aka member functions) and method calls are nothing more than syntactic sugar: we can systematically rewrite them as normal function definitions and normal function calls. At least for "final" methods in Java or non-virtual member functions in C++ (i.e. when there is no polymorphism) this is how things work.
If you look at the "prettier" rewritten version of bar to the right, you see that bar picks out the fields of the object based solely on offsets from the "this" pointer. That's important!
class Foov2 : public Foo //-- in java we'd write: class Foov2 extends Foo
{
public:
bool tag;
void rat() { cout << y << tag << endl; }
};
... and let's suppose that we instantiate a Foov2 object and set
some of its fields and call some of its methods
Foov2 f2; f2.i = 42; f2.j = 23; <--- How does any of this work?!?!? f2.y = 3.5; f2.tag = true; cout << f2.bar() << endl; f2.rat();Let's do a deep dive and see how this might actually work.

| ex2.cpp | running |
$ g++ ex2.cpp $ ./a.out sizeof(f2) = 24 0x7ffe4b8062f0 f2 0x7ffe4b8062f0 f2.i offset 0 0x7ffe4b8062f4 f2.i offset 4 0x7ffe4b8062f8 f2.y offset 8 0x7ffe4b806300 f2.tag offset 16 |
We know that getting/setting fields is really just done by offsets from the "this" pointer, and now we know that those offsets are enchanged for the fields inherited from the base class.
int bar(Foo* this) {
unsigned char* ptr = (unsigned char*)this;
int* iptr = (int*)(ptr + 0); // offset 0
int* jptr = (int*)(ptr + 4); // offset 4
*iptr++;
return *jptr;
}
Now if we execute this code with a pointer to a foov2 object
as its argument, like "bar(&f2)", what happens? Well: f2->i sits
at offset 0 and f2->j sits at offset 4, so ... the function
still works perfectly. It is blissfully unaware that at
offset 16 from &f2 there is more data that pure Foo
objects wouldn't have. It never looks beyond the offsets
of fields in Foo objects.
So inherited methods simply work without any extra shennanigans purely as a by-product of the way new fields of the derived class are contatenated onto the field of the base class.
| ex3part1 | ex3part2 |
| compile | run1 | run2 |
$ g++ ex3.cpp |
$ ./a.out p->bar() = 23 sizeof(*p) = 24 0x7fffbab96040 p 0x7fffbab96048 p->i offset 8 0x7fffbab9604c p->i offset 12 0x7fffbab96050 p->y offset 16 |
$ ./a.out dummy p->bar() = 42 sizeof(*p) = 24 0x7ffe61eea620 p 0x7ffe61eea628 p->i offset 8 0x7ffe61eea62c p->i offset 12 0x7ffe61eea630 p->y offset 16 |
p->bar() -- and sometimes
that call site results in Foo's bar() function getting called,
and other times it results in Foov2's bar() function getting called.
That decision, note, is a runtime decision. The
compiler doesn't know which version of bar() will be called, and
it can change from run to run (or in other examples where we
have loops, it can change within a single run of the program).
Important:
The secret to how this is implemented hinted at by the other
output of this program. Did you notice that the offset of
field i, the first field in the Foo object,
jumped? It went from offset 0 to offset 8. Why? And what do
you think the compiler is doing with those first eight bytes
in a Foo object?
Because we have marked bar() as "virtual" in class Foo, the compiler has to handle polymorphic function calls, and the extra eight bytes that have been inserted into the front of Foo objects is integral to how this is done. Those first eight bytes comprise a pointer to a data structure called the "vtable" for the class.

p->bar() here's what
happens:
| ex4.cpp | running |
$ g++ ex4.cpp $ ./a.out ptr1->bar() = 23 ptr2->bar() = 42 ptr1->bar() = 42 ptr2->bar() = 23 |