Sizes of Types & Class Layout

Low-level SWE interview prep — primitive sizes, struct padding, std::string SSO, and where a class object actually lives

sizeof feels like it should be a simple lookup table, but it's actually the output of three separate decisions stacking on top of each other: the data model of the platform (32-bit vs. 64-bit, LP64 vs. LLP64), the alignment rules the compiler must obey, and — for anything with a constructor or virtual function — the ABI (Itanium C++ ABI on macOS/Linux, MSVC ABI on Windows) that decides where hidden machinery like vtable pointers go. This page works bottom-up: plain types first, then padding, then strings, then full class objects.

Primitive Type Sizes — 64-bit

These are typical sizes under the LP64 data model, which macOS and Linux both use. Nothing here is guaranteed by the C/C++ standard — only minimum ranges are guaranteed — but these are what every mainstream 64-bit compiler actually produces.

TypeSizeAlignmentNotes
char1 byte1Only type the standard guarantees is exactly 1 byte.
bool1 byte1Size not mandated, but universally 1 byte in practice.
short2 bytes2
int4 bytes4Standard only guarantees ≥ short; 4 bytes is just universal convention now.
long8 bytes (macOS/Linux)8The classic gotcha — see callout below.
long long8 bytes8Guaranteed ≥ 64 bits by the standard.
float4 bytes4IEEE-754 single precision.
double8 bytes8IEEE-754 double precision.
long double16 bytes (GCC/Clang, x86-64)1680-bit extended precision, padded to 16; MSVC just makes it identical to double (8 bytes).
T* (any pointer)8 bytes84 bytes on any 32-bit build.
size_t8 bytes8Unsigned, same width as a pointer on that platform.
nullptr_t8 bytes8Same width as a pointer.
Interview trap: long is not portable. macOS and Linux use the LP64 model, where long grows to 8 bytes on 64-bit builds. Windows uses LLP64, where long stays 4 bytes even in a 64-bit build — only long long and pointers get to 8 bytes there. This is exactly why size_t / int64_t / intptr_t exist: they name a *width*, not a keyword whose width silently varies by OS.

Struct Padding & Alignment

Every type has an alignment requirement — the address of any instance of that type must be a multiple of its alignment. For a struct, two rules follow from this: (1) each member must start at an offset that's a multiple of its own alignment, and the compiler inserts padding bytes to make that true, and (2) the struct's overall size must be a multiple of its own alignment (the largest alignment among its members), so that it packs correctly inside an array.

Bad order
struct Bad {
    char   c;   // 1 byte
    int    i;   // 4 bytes
    char   c2;  // 1 byte
    double d;   // 8 bytes
};

sizeof(Bad) == 24

Reordered (packed)
struct Good {
    double d;   // 8 bytes
    int    i;   // 4 bytes
    char   c;   // 1 byte
    char   c2;  // 1 byte
};

sizeof(Good) == 16

Bad — declared char, int, char, double (24 bytes, 8 bytes wasted) c pad(3) int i (4) c2 pad(7) double d (8) offsets: 0 1 4 8 9 16 24 Good — reordered double, int, char, char (16 bytes, only 2 wasted) double d (8) int i (4) c c2 pad(2) offsets: 0 8 12 13 14 16
Ordering members largest-alignment-first minimizes padding. C++ won't reorder members for you within an access-control block — you have to do it in the source.
Why the compiler won't just fix this for you: the C/C++ standard guarantees that members with the same access specifier appear in memory in declaration order. This is deliberate — it's what makes offsetof, C-style struct punning, and network-protocol structs meaningful. So field order is a decision you make, not one the compiler is allowed to optimize away.

std::string — Small String Optimization (SSO)

A hand-rolled "naive" string — int len; char* str; — would be 16 bytes on a 64-bit machine (4 bytes length + 4 padding + 8-byte pointer) and would always heap-allocate, even for a 1-character string. Real std::string implementations are bigger and smarter: they carry a capacity field for amortized growth, and they avoid the heap entirely for short strings by storing the characters directly inside the object.

ImplementationUsed bysizeof(std::string)Max SSO length
libc++Clang / macOS (default)24 bytes22 chars
libstdc++GCC / most Linux distros32 bytes15 chars
MSVC STLWindows32 bytes15 chars
libc++ std::string — one 24-byte union, reinterpreted two ways LONG (heap-backed) capacity (8, +is_long bit) size (8) char* data (8) SHORT (inline, no heap) flag+len inline char buffer — up to 22 characters live directly here (e.g. "Zoey") Same 24 bytes, same address — a discriminator bit picked from the capacity/flag byte tells the library which layout to read. "Zoey" (4 chars) always takes the SHORT path — zero heap allocations.
The 24-byte size is set by the LONG case (capacity + size + pointer, 3 machine words); the SSO buffer just reuses that same space for free.
Not standardized: the C++ standard only specifies std::string's behavior, never its memory layout. SSO thresholds and byte sizes above are implementation details of libc++/libstdc++/MSVC STL respectively — verified with sizeof, not guaranteed by the spec, and technically free to change in a future release (though in practice they've been stable for years).

Class Object Layout — Where Each Piece Lives, and When

Take a polymorphic class with one int, one std::string, and a virtual function:

class ZooAnimal {
public:
    ZooAnimal(const char* n) : name(n) {}
    virtual ~ZooAnimal() {}
    virtual void rotate() {}
    int loc = 0;
    std::string name;
};
What's an ABI? An Application Binary Interface — the compiled-code counterpart to an API. It fixes everything the source language leaves unspecified: struct/vtable layout, calling conventions (which registers hold args/return values), name mangling, etc. Two compilers targeting the same ABI (e.g. Itanium C++ ABI) produce object files that can link together; different ABIs (Itanium vs. MSVC) generally can't.

Under the Itanium C++ ABI (GCC/Clang, i.e. macOS and Linux), a class that introduces virtual functions gets its hidden vtable pointer placed at offset 0 — the very front of the object, not the end. (Older compilers, like the cfront-era ones some textbooks still diagram, put it at the end — that's a real but now largely obsolete convention.)

ZooAnimal object layout — libc++ on macOS, 64-bit (40 bytes total) __vptr (8) loc (4) pad (4) std::string name (24 — cap/size/ptr or inline chars) offset: 0 8 12 16 40 8 (vptr) + 4 (loc) + 4 (pad) + 24 (std::string) = 40 bytes total, 8-byte aligned On libstdc++ (Linux/GCC), swap in a 32-byte std::string → total becomes 48 bytes.
Verify this yourself: reinterpret_cast<char*>(&za) and compare against &za.loc / &za.name to see the real offsets on your machine.

Which segment is each piece actually in?

PieceSegmentSpace allocatedContents filled in
Compiled code for rotate(), ctor, dtor.textCompile timeCompile time — it's just instructions, nothing to "fill in" later.
The vtable (one per class, shared by all instances).rodata / .data.rel.roCompile/link timeStatic linker (non-PIE binaries) resolves addresses at link time; for PIE executables / shared libraries, the dynamic loader patches relocations while mapping the image — before main() runs, but it's the loader doing address patching, not your code executing.
A global object like za.bss / .dataCompile time (fixed address + size)If the constructor does real work (heap alloc, copies), the compiler emits a startup call registered in .init_array, run before main() — this is genuine code execution ("dynamic initialization").
A local object (stack)StackFunction entry (part of the stack frame)Constructor runs the instant control reaches the declaration.
A heap object (new T(...))Heapoperator new call, at runtimeConstructor runs immediately after, on the freshly allocated bytes.
A member subobject (e.g. name inside za)Inline inside the enclosing objectWhenever the enclosing object's storage is allocatedRuns as part of the enclosing object's constructor, in member-declaration order, before the constructor body executes.

Process Address Space — The Big Picture

Stack locals, return addresses — grows downward ↓ unmapped guard region Heap new / malloc — grows upward ↑ .bss zero-initialized globals (e.g. reserved space for za before ctor runs) .data initialized globals (e.g. pza, whose value is a link-time constant) .rodata / .data.rel.ro — vtables, string literals .text — compiled instructions
High addresses at top. Stack and heap grow toward each other; everything below them is fixed in size once the binary is loaded.
SegmentFull form
.bssBlock Started by Symbol
.rodataRead-Only Data
.data.rel.roData, Relocated then Read-Only — writable just long enough for the loader to patch relocations, then remapped read-only

Putting It All Together

Type / objectTypical size (64-bit)Lives inConstructed
int4 byteswherever it's declared (stack/heap/global/inline)N/A — trivial type, no ctor
T*8 byteswherever declaredN/A
Naive {int len; char* str;}16 byteswherever declared; chars always on heapRuns whenever the object is constructed
std::string (libc++)24 bytesobject itself wherever declared; heap only if > 22 charsCtor runs at construction; may or may not touch the heap
std::string (libstdc++)32 bytessame, SSO threshold 15 charsSame
std::vector<int>24 bytes (3 pointers: begin/end/cap)object inline; elements always on heapCtor runs at construction; heap alloc only once you add elements
std::unique_ptr<T>8 bytes (no custom deleter)wherever declaredNo runtime overhead beyond wrapping the raw pointer
std::shared_ptr<T>16 bytes (object ptr + control-block ptr)object inline; control block always on heapControl block heap-allocated at construction (or reused via make_shared)
Polymorphic class w/ vtable (ZooAnimal)40 bytes (libc++) / 48 (libstdc++)wherever declared; vptr points into .rodataCtor runs at construction; vtable itself was already fixed at compile/link time
The vtable itself~1 pointer per virtual function + RTTI/offset slots.rodata / .data.rel.ro — one copy, shared by every instanceFilled in at link time or by the loader before main() — never per-object
Interview payoff: "what's the size of this class" is really three stacked questions — what does the ABI add (vtable pointer, and where), what does alignment force you to pad, and does any member type (like std::string) carry its own hidden bookkeeping. Always verify with sizeof/alignof on the actual target compiler rather than reasoning from a textbook diagram — the numbers are ABI-specific, not standard-specified.