Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

🥽Virtual Lab: Objects on the Stack

This virtual lab explores how class instances (objects) are managed in memory, comparing stack allocation with dynamic heap allocation.

As described in this chapter, every object you declare is, in the end, just a contiguous run of bytes on the stack (or somewhere else in memory) with each field stored in declaration order. Understanding that simple fact unlocks the rest of the language: why classes have a size, why inheritance “is just” laying one struct after another, why some fields appear with strange gaps next to them.

In this virtual lab you’ll explore three classes: Account, DepositAccount, and CurrentAccount, by dragging instances of them onto a simulated stack and observing what happens to the bytes. By the end you’ll be able to predict the size, layout, and starting address of any object made from these classes.

The three classes used throughout the lab are kept simple. Your focus should be on the memory layout, not the behaviour:

class Account {
public:
int accountNumber; // an identifier for the account
double balance; // current balance
};
class DepositAccount : public Account {
public:
double interestRate; // annual interest rate (%)
};
class CurrentAccount : public Account {
public:
double overdraftLimit; // permitted overdraft
};

Before we look at how these classes are laid out in memory, use the tool below to build the inheritance hierarchy for these three classes.

Class Hierarchy

Build the Account Hierarchy

Drag class names from the pool into the slots so each class sits below its parent in the inheritance tree, then click Submit.

Inheritance Tree (base class on top)
·····
·····
·····

Available Classes

Transaction
CreditCard
SavingsAccount
DepositAccount
Bank
CurrentAccount
InterestRate
Account

There are no virtual methods in these classes. That comes in the heap lab that follows. Without virtual, the layout is the simplest possible: just the fields, in order, plus alignment padding.


If any of the words used in this lab feel hazy, here’s a short reference. Skim it now, come back as needed.

TermMeaning
ObjectA specific instance of a class. A region of memory containing all the fields the class declares.
Field(Member Variable) A named variable that lives inside an object; for example, balance inside an Account.
StackThe region of memory where local variables in a function live. Allocation is automatic when the function starts; deallocation is automatic when it returns.
AddressThe location in memory where something lives written as a hexadecimal number like 0x7FFFFFC0. Every object has a starting address.
ByteThe smallest addressable unit of memory, i.e., 8 bits. An int typically takes 4 bytes; a double typically takes 8.
Byte offsetHow far from the start of an object a particular field sits. For example, in Account the accountNumber is at offset +0 and balance is at offset +8.
AlignmentA hardware rule that says certain types must begin at addresses that are multiples of their size. A double is 8-byte aligned: it must start at an address divisible by 8.
PaddingUnused bytes the compiler inserts inside an object so that the next field starts at a properly aligned address.
InheritanceThe mechanism by which one class (the derived class) extends another (the base class). The derived class includes everything from the base, plus any new fields it adds.
Base class(Parent Class) The class being extended, i.e., Account in this lab.
Derived class(Child) The class doing the extending, i.e., DepositAccount and CurrentAccount here.
sizeof(T)A C++ operator that returns the size of any type in bytes, including alignment padding.

Below the guide is the interactive lab itself. The interface has three parts:

  1. The memory strip is a 64-byte slice of the stack, with each little square representing one byte. The leftmost address is shown above (0x7FFFFFC0); each byte to the right is one address higher.
  2. The class palette has three draggable chips at the bottom. Drag any of them onto the strip to instantiate that class at that location. The chip’s badge tells you how many bytes the object will occupy (e.g. Account 16B).
  3. The live code panel is a main.cpp view showing every object currently on the stack as a brace-initialised declaration, plus a cout << obj.field; line for every member of every object.

You can:

  • Drag a class chip onto the strip to create a new object.
  • Click an existing object to edit its name and field values.
  • Click the × badge on any object to remove it.
  • Reset example restores the original two-object starting state.
  • Clear stack removes everything. The lab opens with two objects already on the stack: one Account named a at the start of the region, and one DepositAccount named b immediately after it. The walkthrough below builds on that starting state.

Before you tackle the exercises, take a quick walk through the lab so you know what every part of the picture means.

Look at the lab below. There are two objects: a is an Account sitting at offset 0, and b is a DepositAccount sitting at offset 16. Find these things:

  • The starting address of object a (top-left of the memory strip).
  • The class name and variable name labels above each block.
  • The internal field labels (accountNumber, balance, interestRate) inside each block.
  • The diagonal striped region between accountNumber and balance inside a. That’s padding, not a field you can use.
  • Inside b, the slightly darker region on the left labelled “inherited from Account”. That’s the entire Account sub-object, embedded at the start of the DepositAccount.

Now look at the code panel at the bottom. You’ll see Account a{12345, 100.00}; and DepositAccount b{12345, 100.00, 2.50}; followed by cout lines for every field of both objects. The constructor calls list inherited fields first, then the derived class’s own fields.

If you click anywhere on object a an editor opens. Change the accountNumber to 99999 and the balance to 2500. Click Save.

Notice the code panel updates immediately: Account a{99999, 2500.00}; and the cout << a.balance; line now ends with // output: 2500.00. You haven’t moved any bytes but you’ve just changed what’s stored in them.

Drag the CurrentAccount chip from the palette onto any free area of the memory strip (try around byte 32 or later). The lab will snap it into the next free 24-byte slot.

You should now see three objects in the code panel and three in the strip: a (16 bytes), b (24 bytes), and your new CurrentAccount (24 bytes). Note the byte counter at the top right of the strip. It should now read 64 / 64 bytes used, i.e. the stack region in this simulation is full.

You’re ready for the exercises.


C++ Class Object Lab

Drag a class onto the stack and watch the object's bytes lay out — fields, padding, inherited base and all.

Stack — main()0x7FFFFFC00x7FFFFFFF
40 / 64 bytes used24 free

64-bit platform, double-aligned. Account is 16 bytes (4 + 4 padding + 8); DepositAccount and CurrentAccount add 8 more, so 24 each.

+0
+8
+10
+18
+20
+28
+30
+38
E8
E9
EA
EB
EC
ED
EE
EF
F0
F1
F2
F3
F4
F5
F6
F7
F8
F9
FA
FB
FC
FD
FE
FF
a:Account
accountNumber
12345
pad
balance
100.00
b:DepositAccount
inherited from Account
accountNumber
12345
pad
balance
100.00
interestRate
2.50
Account (own object)derived class (own fields)inherited basealignment padding

Class Palette — drag onto the stack

Account16B
DepositAccount24B
CurrentAccount24B
main.cpp — live
Account a{12345, 100.00}; // 16B at 0x7FFFFFC0
DepositAccount b{12345, 100.00, 2.50}; // 24B at 0x7FFFFFD0
cout << a.accountNumber; // output: 12345
cout << a.balance; // output: 100.00
cout << b.accountNumber; // output: 12345 (from Account)
cout << b.balance; // output: 100.00 (from Account)
cout << b.interestRate; // output: 2.50
Try this:Notice the 4-byte gap between accountNumber and balance — that's alignment padding so the double can sit on an 8-byte boundary. Now drag a DepositAccount onto the stack and watch its first 16 bytes form an inherited Account sub-block, with interestRate appended after. Click any object to edit its fields. The next lab takes the same objects but allocates them on the heap with new — same layout, very different lifetime story.

For each task: read the question, make a prediction, then use the lab to check. Only expand the explanation once you’ve done both. Predicting before checking is what builds intuition; reading the answer first builds none.

Hover over the Account chip in the palette. The badge says 16B. But the class only declares an int (4 bytes) and a double (8 bytes), totalling 12 bytes. Where do the other 4 bytes come from?

In the lab, click on object a and look closely at the bytes between accountNumber (the leftmost field) and balance (the rightmost one). What do you see?

Show explanation

The four bytes between accountNumber and balance are alignment padding, drawn as the diagonal striped region.

A double on a 64-bit platform must begin at an address that’s a multiple of 8. If accountNumber (4 bytes) sits at offset 0, then the next available offset is 4, but that’s not a multiple of 8, so the compiler inserts 4 bytes of unused space, pushing balance out to offset 8 where it can sit aligned.

The total size of the object is therefore:

4 bytes (accountNumber)
+ 4 bytes (padding)
+ 8 bytes (balance)
=16 bytes

In your code, sizeof(Account) would return 16, not 12. Padding is invisible to your program (you can’t read or write it through any field name) but it’s very much real, and it’s why class sizes sometimes seem larger than the sum of their fields.

If you swapped the field order so that balance came first and accountNumber came second, the layout would change: balance at offset 0 (already aligned), accountNumber at offset 8, and 4 bytes of trailing padding to round the whole object up to a multiple of 8. The object would still be 16 bytes. Try this on yourself.

A DepositAccount “is an” Account plus a bit extra. Look at object b in the lab — it’s 24 bytes total. Without doing the maths in code, predict:

  • At what byte offset does interestRate sit inside the DepositAccount object?
  • What’s stored in the bytes before it?
  • Now hover over the inherited region of b and check the editor to verify.
Show explanation

The interestRate sits at byte offset +16 within the DepositAccount object. Everything before it (bytes 0 through 15) is an exact copy of an Account layout: accountNumber at offset 0, padding from 4 to 7, balance at offset 8.

This is what inheritance literally is in C++: the derived class begins with all the bytes of the base class, in the same layout, followed by any new fields it adds. There is no separate “Account part” stored elsewhere. It’s right there at the front of the DepositAccount object.

This has a useful consequence: a pointer to a DepositAccount can also be used as a pointer to an Account, because the Account fields are at exactly the offsets the compiler expects. That’s the foundation of polymorphism, which you’ll meet in the heap lab.

It also explains why if b starts at address 0x7FFFFFD0, then b.accountNumber is at 0x7FFFFFD0, b.balance is at 0x7FFFFFD8, and b.interestRate is at 0x7FFFFFE0.

Reset the lab (use the Reset example button). Object a (an Account) starts at 0x7FFFFFC0. Without looking at b yet, predict:

  • What address does b start at?
  • What address would a third object start at, if you dragged a CurrentAccount in immediately after b? Now drag in a CurrentAccount and check.
Show explanation

Object a is an Account (16 bytes), starting at 0x7FFFFFC0. Therefore it ends at 0x7FFFFFCF (inclusive), and the next byte is 0x7FFFFFD0. That’s where b starts.

b is a DepositAccount (24 bytes), so it occupies 0x7FFFFFD0 through 0x7FFFFFE7. The next byte after b is 0x7FFFFFE8 — that’s where your CurrentAccount should appear.

Hex maths reminder: 0xC0 + 16 decimal = 0xC0 + 0x10 = 0xD0. And 0xD0 + 24 = 0xD0 + 0x18 = 0xE8. Each object’s start address is just the previous object’s start plus the previous object’s size.

In real code you’d compute the address of any local with &objectName, but the rule is the same: locals are placed contiguously on the stack frame.

Task 4: Two objects, same values, different identities

Section titled “Task 4: Two objects, same values, different identities”

Clear the stack. Drag in two Account objects. By default the lab will name them a and b, both with the same default values (accountNumber = 12345, balance = 100.00).

  • Are a and b the same object?
  • If you change b.balance to 999.99, what happens to a.balance? Predict, then check by editing.
Show explanation

No, a and b are different objects. They happen to start with identical contents, but they live at different addresses (0x7FFFFFC0 and 0x7FFFFFD0), so they’re independent storage. Modifying one has no effect on the other.

This is one of the most important things to internalise about classes: the same class definition can produce many distinct objects. The class is the recipe; each object is a separately-baked cake. Two objects of the same class are equal in type but not in identity. They aren’t the same thing, even if their fields happen to match.

In code: Account a, b; declares two separate Account objects. You could later compare &a == &b and the answer would always be false. They are different addresses, and different objects.

The lab’s stack region is 64 bytes. With three objects of the following sizes:

  • Account = 16 bytes
  • DepositAccount = 24 bytes
  • CurrentAccount = 24 bytes How many objects of each type can you fit before the strip is full? Find at least two different combinations that exactly fill all 64 bytes with no gaps.
Show explanation

Some combinations that exactly fill 64 bytes:

  • 4 × Account = 4 × 16 = 64
  • 1 × Account + 2 × DepositAccount = 16 + 48 = 64
  • 1 × Account + 2 × CurrentAccount = 16 + 48 = 64
  • 1 × Account + 1 × DepositAccount + 1 × CurrentAccount = 16 + 24 + 24 = 64 Combinations that don’t fit cleanly leave a remainder less than the smallest object’s size. For example, 3 × DepositAccount = 72 bytes — that’s more than the region. 2 × DepositAccount = 48 bytes, leaving 16 bytes free — exactly the right size for one more Account. So this is also valid: 2 × DepositAccount + 1 × Account.

Try a few of these in the lab. Drag the chips to different locations to see the auto-snap behaviour. If you try to drop an object where it doesn’t fit, the lab refuses the drop and the preview tile turns red.

In real C++, this is exactly what happens to your stack frame: each local variable is given a contiguous run of bytes, and the total stack used by a function is the sum of its locals’ sizes (plus any padding the compiler adds between them for alignment).

Task 6: Address arithmetic from first principles

Section titled “Task 6: Address arithmetic from first principles”

Without using the lab at all, work out the following on paper. Object c is a CurrentAccount starting at address 0x7FFF1000.

  • What’s the address of c.accountNumber?
  • What’s the address of c.balance?
  • What’s the address of c.overdraftLimit?
  • What’s the address of the first byte after c (i.e. where the next variable on the stack would go)? Once you’ve worked it out, set up the lab with a single CurrentAccount and verify the offsets match (the lab uses a different base address, but the offsets from the start of the object will be identical).
Show explanation

Working from a CurrentAccount’s layout:

  • accountNumber is at offset 0 → 0x7FFF1000
  • padding occupies offsets 4–7 (no field name)
  • balance is at offset 8 → 0x7FFF1008
  • overdraftLimit is at offset 16 → 0x7FFF1010
  • The first byte after the object is at offset 24 → 0x7FFF1018 The total size of the object is 24 bytes, so the next variable’s start address is start + 24 = 0x7FFF1000 + 0x18 = 0x7FFF1018.

Notice that the offsets are the same regardless of the starting address. The class layout is fixed at compile time — the only thing that changes from one object to another is the base address you add the offsets to. This is exactly what the compiler does when you write c.balance: it computes &c + 8 and reads 8 bytes from there.

Suppose you write the following code in a real C++ program and run it on a 64-bit machine:

cout << sizeof(Account) << endl;
cout << sizeof(DepositAccount) << endl;
cout << sizeof(CurrentAccount) << endl;

What three numbers does it print? Now, more interestingly, what would happen to those numbers if you added a bool active; field at the end of Account (after balance)?

Predict, then sketch the new layout on paper before reading the explanation.

Show explanation

Without the new field, the output is 16 24 24 — the sizes you’ve already seen.

With bool active; appended after balance, you might guess the new size is 17 bytes (16 + 1 for the bool). It isn’t.

A bool is 1 byte. After balance ends at offset 16, the bool sits at offset 16. The object’s content now ends at offset 17. But the compiler must round the total object size up to a multiple of the largest alignment requirement of any field, which is 8 (for the double). So the actual size becomes 24 bytes: 7 bytes of trailing padding pad it up to the next multiple of 8.

This trailing padding matters when you put objects back-to-back in an array: each object must start at an address that’s properly aligned for all its fields, so its size has to be a multiple of its alignment. Without trailing padding, an array of objects would have misaligned fields after the first one.

DepositAccount and CurrentAccount would now be 32 bytes each (24 inherited + 8 for the new derived field, with no extra padding needed). One added bool increases every object size by 8 bytes. This is why, in performance-critical C++ code, programmers reorder fields from largest to smallest — it minimises padding waste.

This is a preview of the heap lab that follows. Suppose you modified the classes to make the destructor of Account virtual:

class Account {
public:
int accountNumber;
double balance;
virtual ~Account() = default; // NEW: virtual destructor
};

Without changing any field, what happens to:

  • sizeof(Account)?
  • The byte offset of accountNumber?
  • The byte offset of balance? Sketch the new layout. The lab in this exercise doesn’t show this case (that’s the next lab) but you should be able to predict it.
Show explanation

Adding any virtual method (including a virtual destructor) causes the compiler to insert a hidden 8-byte pointer at offset 0 of the object — the vtable pointer, often called the __vptr. It points at a small read-only table of function pointers (the vtable) that the runtime uses to look up which version of a virtual method to call.

The new layout becomes:

+ 0 8 bytes `__vptr` (hidden, points at Account::vtable)
+ 8 4 bytes accountNumber
+12 4 bytes padding
+16 8 bytes balance
— total: 24 bytes

So:

  • sizeof(Account) becomes 24 (was 16).
  • accountNumber moves from offset 0 to offset 8.
  • balance moves from offset 8 to offset 16. Every object pays this 8-byte cost regardless of which virtual methods you add. It’s a one-time-per-object overhead for having any virtual method. DepositAccount and CurrentAccount would correspondingly grow to 32 bytes each.

This is the price of polymorphism, and the next lab makes it visible by allocating these objects on the heap, where you can also see the vtable itself sitting in static memory and the __vptr inside each object pointing at it.


You’ve now seen, by direct manipulation of bytes, that:

  • A class is a layout description; an object is a concrete run of bytes laid out according to that description.
  • The size of an object is the sum of its fields’ sizes plus any padding the compiler inserts to satisfy alignment, plus any trailing padding to round the total up to a multiple of its alignment.
  • Inheritance is implemented by embedding the base class’s layout at the start of the derived class’s layout, which means a derived object literally begins with a fully-formed base object in its first bytes.
  • Two objects of the same type are independent storage at distinct addresses, even if their fields happen to be equal.
  • Address arithmetic on objects is just base address + field offset. The compiler computes offsets at compile time and adds them to the runtime base address. In the next lab you’ll move these same three classes onto the heap with new, add virtual methods, and discover what the vtable pointer looks like in practice, along with the three classic heap bugs (leak, dangling pointer, double-delete).
Concept Match

Match the Memory Layout Concepts

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

Padding
drag a definition here…
Stack
drag a definition here…
Inheritance
drag a definition here…
sizeof
drag a definition here…

Definition Pool

The memory region where local variables live; allocation and deallocation are automatic.
Unused bytes inserted by the compiler to satisfy hardware alignment rules.
An operator that returns the total size of an object in bytes, including all fields and padding.
A mechanism where a derived class embeds the base class fields at its start (offset +0).
Knowledge Check

Why is sizeof(Account) 16 bytes when its fields (int and double) only total 12 bytes?

Knowledge Check

If you have two objects of the same class with identical field values, what can you say about their identity?

Knowledge Check

Where are the fields of a base class located within a derived class object?

Knowledge Check

What happens to the stack memory of a local object when the function returns?