all_lessons/C++/02 · Objects & the memory modellesson 3 / 20

Objects, values, and where they live: the memory model

Lesson 01 established that a running C++ program is machine code and data sitting in one big array of addressable bytes, with no interpreter in the way. That raises the obvious next question: what, exactly, are the things your program puts in that memory, and where in it do they go? This lesson answers both. The one new idea: in C++ an object is not "an instance of a class" — it is a typed region of storage, a run of bytes that some type tells you how to read. We separate the value from the storage that holds it, measure the storage with sizeof, watch unsigned arithmetic wrap around, and draw the stack frame that makes a local variable nearly free to create.

The thesis, here
The cost-model habit starts with question two from Lesson 00 — where does the memory live? — and you cannot answer it until you know what "a thing in memory" even is. C++ makes the bill itemized here by being literal: every object occupies a known number of bytes (sizeof tells you exactly how many) in one of three places with three different cost and lifetime profiles. Nothing is boxed, nothing is hidden behind a reference you didn't ask for; an int is four bytes, right there, and you can point at them.
Linear position
Prerequisite: Lesson 01 — you know a program is code plus data in addressable memory, that the compiler turns each translation unit into machine code, and that there is no runtime standing between your variables and the CPU. We now ask what those variables are at the byte level.
New capability: define a C++ object precisely (typed storage), separate value from storage, read the size of any type with sizeof, recognize integer overflow as undefined or wrapping behavior, and name where any variable lives — stack, heap (Lesson 04), or static — and what creating it costs.
The plan
Five moves. (1) Define object the way the C++ standard does, and split value from storage. (2) Measure the fundamental types with sizeof — concrete byte counts. (3) Signed vs unsigned and integer overflow, with a bug that bites real programs. (4) The three storage durations — automatic, dynamic, static — and which is which. (5) Draw the stack frame explicitly and trace nested calls pushing and popping, which is why a local costs almost nothing.

1 · What an "object" actually is

In Python or Java, "object" means "instance of a class," and a plain integer is something else. C++ uses the word far more broadly and more precisely. An object is a region of storage with a type. That is the whole definition. An int is an object. A double is an object. A char is an object. So is an instance of your own class. The type is not decoration: it tells the compiler two things — how many bytes the region occupies, and how to interpret the bit pattern in those bytes (as a signed integer, as a floating-point number, as a character code).

The crucial separation — the one this lesson exists to install — is between the value and the storage that holds it. The storage is a fixed run of bytes at some address. The value is the meaning of the bit pattern currently sitting in those bytes. The variable x below names a region of storage; 42 is the value that occupies it; assigning x = 43 does not move x anywhere — it overwrites the bytes in place with a new bit pattern.

int x = 42;   // 'x' names a 4-byte region of storage; its value is 42.
x = 43;       // same storage, same address; the bytes now encode 43.
int y = x;    // a SECOND 4-byte region; the bit pattern of x is COPIED into it.
              // y and x are independent storage. Changing one cannot touch the other.

That last line is the seed of value semantics (Lesson 06): by default, assignment and argument-passing in C++ copy the bytes, producing a second independent object — unlike Python, where y = x for a list makes y a second name for the same object. Holding "object = typed storage" in mind is what makes that copy visible instead of surprising. An object also has a lifetime — a moment it begins (storage obtained, value initialized) and a moment it ends (storage reclaimed). Where the object lives decides how long that lifetime is and who controls it, which is §4.

Storage without a value is a trap
Obtaining storage and giving the object a value are separate steps. int x; at function scope creates the 4-byte region but does not initialize it — the value is whatever bytes were left there by whatever used that stack slot last. Reading x before assigning it is undefined behavior (the contract from Lesson 00; the catalog is Lesson 19): the program may print garbage, may print 0, may behave differently under the optimizer. Always initialize: int x = 0; or int x{};. The storage being there does not mean the value is.

2 · Fundamental types and their sizes — measure with sizeof

Because every object is a known number of bytes, you can ask for that number directly. sizeof(T) is a compile-time expression giving the size in bytes of any type T (a byte is the smallest addressable unit, almost always 8 bits). It costs nothing at runtime — the answer is baked in when the program is compiled. Here are the workhorse fundamental types on a typical 64-bit desktop platform:

TypeTypical sizeofHoldsNote
bool1true / false1 byte even though 1 bit would do — bytes are the addressable unit
char1one byte / a character codesizeof(char) is 1 by definition; all other sizes are multiples of it
int4whole numbers ≈ ±2.1 billionthe default integer; range is roughly −2,147,483,648 … 2,147,483,647
double8floating-point realsthe default for non-integers; float is 4 bytes, less precise
long long8whole numbers ≈ ±9.2 quintillionwhen 4 bytes isn't enough range
void*8a memory address (Lesson 03)8 bytes on a 64-bit machine — that's what "64-bit" means
Worked example — print the sizes
#include <cstdio>
int main() {
    std::printf("bool      %zu\n", sizeof(bool));        // 1
    std::printf("char      %zu\n", sizeof(char));        // 1
    std::printf("int       %zu\n", sizeof(int));         // 4
    std::printf("double    %zu\n", sizeof(double));      // 8
    std::printf("long long %zu\n", sizeof(long long));   // 8
    std::printf("int*      %zu\n", sizeof(int*));        // 8  (a pointer is an address)
    int arr[10];
    std::printf("arr       %zu\n", sizeof(arr));         // 40 = 10 * sizeof(int)
}
The numbers in the comments are what a 64-bit machine prints. Notice sizeof(arr) is 40 — an array is contiguous storage, ten 4-byte ints laid end to end with nothing between them (that contiguity is what makes arrays and vector cache-friendly — Lesson 08). The sizes are not guaranteed by the standard except sizeof(char) == 1 and the ordering; they are properties of the platform, which is exactly why C++ gives you sizeof instead of asking you to memorize them.

3 · Signed, unsigned, and overflow — a real hazard

An integer type is a fixed number of bytes, so it can represent only a finite range of values. A 4-byte int has 32 bits, which is 2³² ≈ 4.29 billion distinct bit patterns. A signed integer spends one of those patterns' worth of range on the sign, splitting the range roughly in half around zero: about −2.1 billion to +2.1 billion. An unsigned integer (unsigned int) gives up negatives entirely and uses the full range for non-negatives: 0 to about 4.29 billion. Same 32 bits, different interpretation — and the interpretation is the type.

What happens when a computation lands outside the range? This is integer overflow, and the two signednesses behave differently — a distinction that traps people:

Unsigned: wraps around (defined)

Unsigned arithmetic is defined to wrap modulo 2ⁿ — it counts like a car odometer rolling over. 0u - 1u is not −1; it is the largest unsigned value, 4,294,967,295. This is well-defined, but it is rarely what you meant.

Signed: undefined behavior

Signed overflow is undefined behavior (Lesson 19). INT_MAX + 1 has no defined result; the compiler is allowed to assume it never happens and optimize on that assumption. It does not reliably wrap, even though the hardware often would.

Worked bug — the unsigned underflow loop
This compiles, runs, and is wrong — a classic that has shipped in real code:
#include <vector>
#include <cstdio>
void print_reverse(const std::vector<int>& v) {
    // v.size() returns an UNSIGNED type (std::size_t).
    // We want to count down to and including index 0.
    for (std::size_t i = v.size() - 1; i >= 0; --i)   // BUG
        std::printf("%d ", v[i]);
}
Trace it for a 3-element vector. i walks 2, 1, 0 — printing correctly. Then --i runs once more. Because i is unsigned, 0 - 1 does not become −1; it wraps to 18,446,744,073,709,551,615 (the largest 8-byte unsigned value). The condition i >= 0 is therefore always true for an unsigned i — it can never be negative — so the loop never ends, and v[huge_index] reads wildly out of bounds (itself undefined behavior). The deeper bug: if the vector is empty, v.size() - 1 underflows on the very first line. The fix is to never mix "count down to zero" with an unsigned counter — iterate forward, use a signed index, or use a range-based loop. The whole bug is invisible unless you remember the type is unsigned and that its range has a hard floor at 0.

The lesson under the bug: a type is a contract about a finite set of bit patterns and how arithmetic on them behaves. Prefer int (or std::ptrdiff_t) for quantities that can be subtracted toward or below zero; reach for unsigned only when you specifically want modular wraparound or are interfacing with sizes, and even then count carefully near the boundaries.

4 · The three places an object can live

Every object in C++ has a storage duration — a rule for when its storage is obtained and reclaimed. There are three you will meet constantly, and they have wildly different cost and lifetime profiles. Naming which one an object has is most of answering "where does the memory live?" and "when does it die?"

automatic — the stack
A local variable inside a function. Its storage is obtained when control enters its scope and reclaimed automatically when the scope exits. Lives on the call stack (§5). Creation is nearly free. This is the default and where most objects should live.
dynamic — the heap
Storage you request explicitly (new, or a container internally) when you need a lifetime not tied to a scope, or a size unknown until runtime. It lives until you (or an owner) release it. Flexible but the costly, error-prone one — the entire subject of Lesson 04.
static — lives forever
Globals and static locals. Storage exists for the whole run of the program, in a fixed region established at load time (Lesson 01). One copy, created before main (or on first use), reclaimed at exit. No per-call cost because there are no per-call instances.
int g_total = 0;                 // STATIC: one copy, lives the whole program run.

int sum_to(int n) {
    int acc = 0;                 // AUTOMATIC: fresh each call, dies when sum_to returns.
    static int call_count = 0;   // STATIC: created once, KEEPS its value across calls.
    ++call_count;
    for (int i = 1; i <= n; ++i) // 'i' is AUTOMATIC, scoped to the loop body.
        acc += i;
    // int* p = new int(acc);    // DYNAMIC: heap storage, would outlive this call
                                 //          and must be freed by someone (Lesson 04).
    return acc;                  // 'acc' and 'i' are reclaimed here; call_count persists.
}

Notice that acc and i simply cease to exist when sum_to returns — you do not free them, and there is no garbage collector deciding later. Their reclamation is part of the function returning. call_count, marked static, sits in the static region and survives every call, accumulating. That automatic-reclamation-at-scope-exit is the single most important property of stack storage, and Lesson 05 (RAII) turns it into the central tool of the language.

5 · The stack frame, drawn — and why locals are nearly free

Automatic objects live on the call stack: one contiguous region of memory that grows and shrinks as functions call and return. When a function is called, the machine reserves a slab of that region for it — a stack frame (also "activation record"). The frame holds the function's automatic locals, the return address (where to resume in the caller once this function finishes — recall from Lesson 01 that code is just addresses in memory), and bookkeeping. Calling pushes a frame; returning pops it.

"Push" and "pop" are almost literal and almost free. The CPU keeps the current top of the stack in a register, the stack pointer. To allocate a whole frame's worth of locals, it subtracts the frame size from the stack pointer — one arithmetic instruction. To free the entire frame on return, it adds the size back — one instruction. There is no search for free space, no bookkeeping list, no per-object cost: allocating ten locals or one is the same single pointer adjustment. That is what "stack allocation is nearly free" means, and it is why C++ steers you toward automatic storage by default.

Worked trace — frames push and pop for nested calls
Consider this chain, and trace the stack at the moment we are deepest inside square:
int square(int x)       { int r = x * x;        return r; }   // (C)
int sum_squares(int a)  { int s = square(a) + 1; return s; }  // (B)
int main()              { int n = 3;
                          int out = sum_squares(n); }         // (A)
The stack grows downward in this drawing (lower in the picture = more recently pushed = higher address-wise on a typical machine grows the other way, but the order is what matters). At the instant square is computing r, three frames are live:
[ main frame ] n = 3, out = (not set yet) return address -> (the OS / C runtime) | v main called sum_squares [ sum_squares frame ] a = 3, s = (not set yet) return address -> back into main, at "out = ..." | v sum_squares called square [ square frame ] <-- stack pointer here, this is the top x = 3, r = 9 return address -> back into sum_squares, at "+ 1"
Now square returns 9. Its frame is popped — the stack pointer moves back up to the bottom of sum_squares's frame in one instruction, and x and r are gone. Control resumes at square's return address inside sum_squares, which computes s = 9 + 1 = 10. Then sum_squares returns 10, its frame pops, and back in main we land at the return address and store out = 10. Each call pushed exactly one frame; each return popped exactly one; at no point did anyone "free" x, r, a, or s by hand. The geometry is strictly last-in-first-out, which is precisely why it can be a single moving pointer.

Two consequences fall out of this picture. First, a pointer or reference to a local (Lesson 03) becomes dangling the moment that local's frame is popped — returning the address of r from square hands the caller a pointer into reclaimed storage, a use-after-free in waiting (Lesson 04). Second, the stack is finite (often a few megabytes): unbounded recursion pushes frames forever and eventually runs off the end — stack overflow. Both are direct, predictable consequences of "frames push and pop," not mysteries.

Common mistakes / failure modes

Reading uninitialized storage
int x; use(x); — the storage exists but holds leftover bytes. Undefined behavior. Initialize at declaration: int x{};.
Unsigned counting below zero
for (size_t i = n-1; i >= 0; --i) never terminates and underflows when n == 0 (§3). Unsigned has no negatives; i >= 0 is always true.
Returning the address of a local
The local's frame is popped on return; the address now points at reclaimed stack. The pointer dangles (§5, Lesson 04).
Assuming fixed sizes
int is not guaranteed 4 bytes, nor pointers 8. Only sizeof(char)==1 is fixed. Use sizeof / fixed-width types (int32_t) when the count matters.
Signed overflow as "it wraps"
Signed overflow is UB, not modular wraparound. The optimizer may delete an if (x + 1 < x) check entirely because it "can't happen" (Lesson 19).
Confusing value with storage
y = x copies the bytes into a second object; it does not alias x. Forgetting this hides copy costs (Lesson 06) for big objects.

Checkpoint exercise

Try it
Predict, then verify. (1) Write the sizeof program from §2 and run it — do int, double, and int* match the table on your machine? What is sizeof of a struct { char c; int i; };? (It is probably 8, not 5 — the reason is alignment and padding, which is Lesson 08; just note the surprise now.) (2) Run the reverse-print bug from §3 on a 3-element vector under -fsanitize=undefined,address (UBSan/ASan, the tooling from Lesson 00) and read the out-of-bounds report. (3) Take the nested-call trace from §5 and add a fourth level (have square call a helper); draw the four live frames at the deepest point, then mark which locals are gone after each return. The goal is to be able to say, for any variable, which of the three places it lives and exactly when its storage is reclaimed.

Where this points next

We can now name where any object lives and what its storage costs — but §5 left a loose thread we waved at twice: the moment you take the address of an object, you are holding something new, a value that refers to storage rather than being it. That is a pointer, and it is the mechanism behind dangling references, arrays, sharing, and every form of indirection in the language. Lesson 03 makes the address itself a first-class value you can store, copy, and dereference — & and *, nullptr, the tie between a[i] and *(a+i), and references as un-rebindable aliases. Once an address is a value, the lifetime question from §4 gets sharp teeth, and Lesson 04 can finally pose the heap's central problem: who frees this, and when?

Takeaway
In C++ an object is a typed region of storage — a known run of bytes (sizeof tells you how many: typically 1 for char/bool, 4 for int, 8 for double and pointers) that a type tells you how to interpret — and the value is just the bit pattern currently in that storage, separate from the storage itself, which is why y = x copies bytes into an independent object. Because storage is finite, integers overflow: unsigned wraps modulo 2ⁿ (defined, and the cause of the classic never-ending size_t countdown loop), while signed overflow is undefined behavior. Every object has one of three storage durations — automatic (stack, reclaimed at scope exit, nearly free because creating a frame is one pointer subtraction), dynamic (heap, lifetime you manage — Lesson 04), or static (lives the whole run) — and the stack frame, pushed on call and popped on return in strict last-in-first-out order, is what makes locals cheap and also what makes a returned pointer-to-local dangle.

Interview prompts