Skip to main content

Class Memory Layout

Understanding how classes are laid out in memory is essential for optimization, debugging, and interfacing with other languages.

Memory Organization

Members are arranged in declaration order with padding for alignment. Virtual functions add hidden vtable pointers.

Basic Class Layout

Members appear in memory in declaration order, never reordered by the compiler. Padding is inserted to meet alignment requirements.

class Simple {
int a; // 4 bytes, offset 0
char b; // 1 byte, offset 4
// 3 bytes padding
int c; // 4 bytes, offset 8
};

sizeof(Simple); // 12 bytes (not 9!)

Memory layout:

Offset  Size  Member
0 4 int a
4 1 char b
5 3 [padding]
8 4 int c
Total: 12 bytes

The padding after b ensures c starts at an address divisible by 4 (its alignment requirement). The compiler never reorders members to minimize padding - they appear exactly in declaration order. This predictability is important for binary compatibility and interfacing with C.

Member Order Matters

Reordering members from largest to smallest minimizes padding waste.

// ❌ Wasteful: 32 bytes
class Wasteful {
char a; // 1 byte + 7 padding
double b; // 8 bytes
char c; // 1 byte + 7 padding
double d; // 8 bytes
};

// ✅ Efficient: 24 bytes (25% smaller)
class Efficient {
double b; // 8 bytes
double d; // 8 bytes
char a; // 1 byte
char c; // 1 byte
// 6 bytes padding (to make size multiple of 8)
};
Optimization Rule

Group by size: doubles together, ints together, chars together. This minimizes padding between members.

Alignment Requirements

Each type has an alignment requirement - the address must be divisible by this value.

alignof(char);    // 1
alignof(short); // 2
alignof(int); // 4
alignof(double); // 8
alignof(void*); // 8 (on 64-bit)

class Example {
char c; // Align: 1
int i; // Align: 4
double d; // Align: 8
};

alignof(Example); // 8 (max alignment of members)
sizeof(Example); // 16 (must be multiple of alignment)

Alignment determines:

  • Where members can be placed (must be at aligned addresses)
  • Overall class size (must be multiple of max member alignment)
  • Performance (aligned access is faster)

Empty Classes

Even empty classes have non-zero size to ensure distinct addresses for different objects.

class Empty {};
sizeof(Empty); // 1 byte (minimum)

Empty arr[10];
&arr[0] != &arr[1]; // Must have different addresses

// Empty base optimization
class Derived : Empty {
int value;
};
sizeof(Derived); // 4 bytes (Empty takes no space as base)
Why Size 1?

C++ requires every object to have a unique address. Zero-size objects would violate this. However, base classes can be optimized away (EBO).

Member Access and Offsets

The compiler calculates fixed offsets for each member, enabling efficient direct access.

class Widget {
int x; // Offset: 0
int y; // Offset: 4
int z; // Offset: 8
};

Widget w;
int* px = &w.x; // Address of w + 0
int* py = &w.y; // Address of w + 4
int* pz = &w.z; // Address of w + 8

// Member access compiles to:
// w.y → *(address_of_w + 4)

Member access is just pointer arithmetic using compile-time offsets. This makes member access as fast as array indexing. The offsets are fixed at compile-time and never change, enabling aggressive optimization.

Virtual Functions and vtables

Classes with virtual functions have a hidden vtable pointer (vptr) at the start of the object.

class NoVirtual {
int data;
};
sizeof(NoVirtual); // 4 bytes

class WithVirtual {
int data;
virtual void f() {}
};
sizeof(WithVirtual); // 16 bytes (8 vptr + 4 data + 4 padding)

vtable overhead:

[vptr: 8 bytes] → points to vtable
[int data: 4 bytes]
[padding: 4 bytes]
Total: 16 bytes

The vptr points to a table of function pointers used for dynamic dispatch. Every object of a class with virtual functions has its own vptr pointing to the shared vtable for that class.

vtable Structure

The vtable contains function pointers for all virtual functions in the class hierarchy.

class Animal {
public:
virtual void speak() { std::cout << "Animal\n"; }
virtual void move() { std::cout << "Moving\n"; }
};

class Dog : public Animal {
public:
void speak() override { std::cout << "Woof\n"; } // Overridden
// move() inherited
};

// Animal's vtable: Dog's vtable:
// [0] → Animal::speak [0] → Dog::speak (overridden)
// [1] → Animal::move [1] → Animal::move (inherited)

Virtual call mechanism:

Animal* ptr = new Dog();
ptr->speak();

// Compiled to approximately:
// 1. Load vptr from object
// 2. Load function pointer from vtable[0]
// 3. Call that function

Virtual function calls require an extra indirection: load the vptr, then load the function pointer, then call it. This is slower than non-virtual calls but enables polymorphism. The vtable itself is shared among all objects of the same dynamic type.

Multiple Inheritance Layout

Multiple base classes create multiple subobjects within the derived class.

class Base1 {
int b1;
};

class Base2 {
int b2;
};

class Derived : public Base1, public Base2 {
int d;
};

sizeof(Derived); // 12 bytes

Layout:

[Base1: b1: 4 bytes]
[Base2: b2: 4 bytes]
[Derived: d: 4 bytes]

Each base class appears as a subobject within the derived object. The derived class members appear after all base class members. Pointer conversions between derived and base classes may require address adjustments to point to the correct subobject.

Multiple Inheritance with Virtual Functions

When multiple base classes have virtual functions, the derived class contains multiple vtable pointers.

class Base1 {
virtual void f1() {}
};

class Base2 {
virtual void f2() {}
};

class Derived : public Base1, public Base2 {
int d;
};

sizeof(Derived); // 32 bytes on 64-bit

Layout:

[vptr1: 8 bytes] → Base1's vtable
[Base1 data]
[vptr2: 8 bytes] → Base2's vtable
[Base2 data]
[Derived data]
[padding]

Each base class with virtual functions contributes a vtable pointer. Converting Derived* to Base2* requires adjusting the pointer to skip over the Base1 subobject. This adds complexity but enables polymorphism through any base class.

Alignment and Padding

The class's overall alignment is the maximum alignment of its members.

class Example {
char c; // Align: 1
int i; // Align: 4
double d; // Align: 8
};

alignof(Example); // 8 (from double)
sizeof(Example); // 16

// Layout:
// [char c: 1][padding: 3][int i: 4][double d: 8]
// Total must be multiple of alignment (16 = 2 * 8)

The size must be a multiple of the alignment to ensure arrays of objects maintain proper alignment for all members. This sometimes requires trailing padding after the last member.

Bit Fields

Bit fields allow packing multiple values into fewer bytes, useful for flags and low-level structures.

struct Flags {
unsigned int flag1 : 1; // 1 bit
unsigned int flag2 : 1; // 1 bit
unsigned int value : 6; // 6 bits
// All 8 bits fit in 1 byte
};

sizeof(Flags); // 4 bytes (implementation-defined, often padded to int)

Flags f;
f.flag1 = 1;
f.flag2 = 0;
f.value = 42;

Bit fields specify the exact number of bits for each member. The compiler packs them together to save space. However, bit fields have limitations: you can't take their address, and layout is implementation-defined. Use them sparingly for memory-constrained scenarios.

Structure Packing

Some compilers allow forcing tighter packing, eliminating padding but potentially causing misalignment.

// GCC/Clang: packed attribute
struct __attribute__((packed)) Packed {
char c; // 1 byte
int i; // 4 bytes (no padding before!)
char c2; // 1 byte
};
sizeof(Packed); // 6 bytes (no padding)

// MSVC: pragma pack
#pragma pack(push, 1)
struct Packed {
char c;
int i;
char c2;
};
#pragma pack(pop)
sizeof(Packed); // 6 bytes

Packed structures eliminate padding but members may be misaligned. On x86-64 this is slow; on ARM it can crash. Only use packing for binary file formats or network protocols where you need exact layout. Never use for normal program data.

Standard Layout Classes

Standard layout classes have restrictions but guarantee C-compatible memory layout.

// Standard layout: C-compatible
class StandardLayout {
public:
int a;
int b;
};

// Not standard layout: different access control
class NotStandard {
private:
int a;
public:
int b;
};

// Not standard layout: virtual functions
class NotStandard2 {
virtual void f() {}
int a;
};

static_assert(std::is_standard_layout_v<StandardLayout>);

Standard layout classes can be passed to C code because their layout is predictable and compatible. They require: no virtual functions, no virtual base classes, same access control for all non-static members, no non-standard-layout base classes or members.

Inspecting Layout

Use offsetof and compiler tools to examine exact memory layout.

#include <cstddef>

class Widget {
public:
char a;
int b;
double c;
};

// offsetof only works for standard-layout types
offsetof(Widget, a) // 0
offsetof(Widget, b) // 4
offsetof(Widget, c) // 8

sizeof(Widget); // 16
alignof(Widget); // 8

Compiler tools:

# GCC/Clang: dump class hierarchy
g++ -fdump-class-hierarchy file.cpp

# Clang: dump record layouts
clang++ -Xclang -fdump-record-layouts file.cpp

# MSVC: dump all class layouts
cl /d1reportAllClassLayout file.cpp

Summary

Core principles:
  • Members appear in declaration order (never reordered)
  • Padding inserted for alignment requirements
  • Size must be multiple of alignment
  • Virtual functions add 8-byte vptr (64-bit)
Alignment rules:
  • Each type has alignment requirement (1, 2, 4, 8 bytes)
  • Class alignment = max member alignment
  • Size = multiple of alignment (for arrays)
Optimization:
  • Order members largest → smallest to minimize padding
  • Group related data for cache performance
  • Empty base optimization (EBO) eliminates empty base size
Virtual function overhead:
  • 8 bytes per object (vptr)
  • Multiple inheritance = multiple vptrs
  • ~2-3ns per virtual call (indirect through vtable)
Tools:
  • sizeof() - total size including padding
  • alignof() - alignment requirement
  • offsetof() - member offset (standard-layout types only)
  • Compiler flags - dump exact layout