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.
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.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.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.
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:
| Type | Typical sizeof | Holds | Note |
|---|---|---|---|
bool | 1 | true / false | 1 byte even though 1 bit would do — bytes are the addressable unit |
char | 1 | one byte / a character code | sizeof(char) is 1 by definition; all other sizes are multiples of it |
int | 4 | whole numbers ≈ ±2.1 billion | the default integer; range is roughly −2,147,483,648 … 2,147,483,647 |
double | 8 | floating-point reals | the default for non-integers; float is 4 bytes, less precise |
long long | 8 | whole numbers ≈ ±9.2 quintillion | when 4 bytes isn't enough range |
void* | 8 | a memory address (Lesson 03) | 8 bytes on a 64-bit machine — that's what "64-bit" means |
#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 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 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.
#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?"
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 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.
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:
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
int x; use(x); — the storage exists but holds leftover bytes. Undefined behavior. Initialize at declaration: int x{};.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.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.if (x + 1 < x) check entirely because it "can't happen" (Lesson 19).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
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?
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
- Define "object" as the C++ standard means it. (§1 — a region of storage with a type; the type fixes the byte count and how the bit pattern is interpreted. An
intis an object, not just class instances.) - What is the difference between a value and the storage that holds it? (§1 — storage is the fixed run of bytes at an address; the value is the meaning of the bits currently there.
y = xcopies the bit pattern into separate storage, giving an independent object — value semantics, Lesson 06.) - What does
sizeofreturn, and what is guaranteed about it? (§2 — a compile-time size in bytes; onlysizeof(char)==1and that other sizes are multiples of it are guaranteed.int≈4,double≈8, pointer≈8 are typical, not mandated.) - Why does
for (size_t i = v.size()-1; i >= 0; --i)loop forever? (§3 —size_tis unsigned and can never be < 0, soi >= 0is always true;--iat 0 wraps to the max value, and an empty vector underflows on the first line.) - How does signed overflow differ from unsigned overflow? (§3 — unsigned wraps modulo 2ⁿ (well-defined); signed overflow is undefined behavior, so the compiler may assume it never happens and optimize accordingly — Lesson 19.)
- Name the three storage durations and when each object's storage is reclaimed. (§4 — automatic: at scope exit; dynamic: when explicitly released, lifetime you manage (Lesson 04); static: at program exit, existing the whole run.)
- Why is stack allocation nearly free, and what two failure modes follow from the frame model? (§5 — a frame is allocated/freed by adjusting the stack pointer by the frame size, one instruction regardless of count; the consequences are dangling pointers to popped locals and stack overflow from unbounded recursion.)