Part 1 — From Source Code to a Running Program
1.1 The Four-Stage Build Pipeline
Unlike JavaScript or Python, C++ doesn't get read and run line by line. It goes through a strict pipeline before a single instruction executes:
-
Preprocessing — handles everything starting with
#.#includeliterally pastes the header file's text in.#definedoes text substitution.#ifdef/#ifndefstrip out code conditionally. Output: a single expanded "translation unit." -
Compilation — the compiler (GCC, Clang, MSVC) parses the translation unit into an AST, performs type checking, and generates assembly, then object code (
.o/.obj) — machine code specific to your CPU architecture, but with unresolved references to functions/variables defined elsewhere. -
Linking — the linker takes all your object files plus library code (
.lib/.a/.so/.dll) and resolves every reference, producing a single executable. This is where you get the dreaded "undefined reference" error — the compiler was happy, but the linker couldn't find where a function is actually defined. -
Loading — the OS loader reads the executable, maps it into a new process's address space, sets up the stack/heap, and jumps to
main().
// file: main.cpp
#include <iostream> // preprocessor pastes iostream's contents here
int square(int); // declaration — compiler trusts this exists
int main() {
std::cout << square(5); // compiler emits a CALL instruction to an unresolved symbol
return 0;
}
int square(int x) { return x * x; } // definition — linker resolves the CALL to here
This is why C++ separates declaration (a promise: "this exists, here's its signature") from definition (the actual implementation) — and why header files traditionally hold declarations while .cpp files hold definitions.
1.2 Why This Matters in Practice
- Compile-time errors (syntax, type mismatches) happen in stage 2.
- Link-time errors (missing implementations, duplicate definitions) happen in stage 3.
- Runtime errors (segfaults, division by zero) happen only after stage 4, while your program executes.
Interviewers love asking "is this a compile-time or runtime error?" — knowing this pipeline answers it instantly.
Part 2 — The Memory Model (the single most important section)
C++'s entire identity — performance, control, and its footguns — comes from the fact that you manage memory instead of a garbage collector. A running C++ process's memory is divided into distinct regions:
High addresses
+-------------------+
| Stack | <- grows downward; local variables, function frames
| | |
| v |
| |
| ^ |
| | |
| Heap | <- grows upward; dynamic memory (new/malloc)
+-------------------+
| BSS segment | <- uninitialized global/static variables
+-------------------+
| Data segment | <- initialized global/static variables
+-------------------+
| Text/Code segment | <- your compiled machine instructions (read-only)
+-------------------+
Low addresses
2.1 The Stack
- Every function call pushes a stack frame: parameters, local variables, the return address.
- Allocation/deallocation is just moving a pointer (the stack pointer) — this is why stack memory is extremely fast.
- Memory is automatically reclaimed the instant a function returns — this is the mechanism behind RAII (see 2.4).
- Fixed, limited size (often 1-8MB) — deep or infinite recursion causes a stack overflow.
void foo() {
int x = 10; // pushed onto foo's stack frame
int arr[1000]; // also on the stack — large local arrays are a common overflow cause
} // frame popped automatically here — x and arr are gone
2.2 The Heap (Free Store)
- Memory you explicitly request with
new(ormallocin C-style code) and must explicitly release withdelete(orfree). - Slower than the stack — the allocator has to search for a free block of the right size and track it.
- Survives function returns — this is the entire reason it exists: to create data whose lifetime isn't tied to a single function call.
- Not automatically cleaned up. Forgetting
deleteis a memory leak. Callingdeletetwice, or using memory after deleting it, is undefined behavior (a dangling pointer / use-after-free).
int* createOnHeap() {
int* p = new int(42); // allocated on heap, survives this function returning
return p;
}
int main() {
int* p = createOnHeap();
std::cout << *p; // valid — heap memory outlived createOnHeap()
delete p; // YOUR job — nobody does this for you
p = nullptr; // good practice: avoid a dangling pointer
}
2.3 Pointers vs References — What's Actually Different
Both let you refer to another variable's memory, but they compile to very different things:
| Pointer | Reference | |
|---|---|---|
| Can be reassigned | Yes | No — bound once, forever |
| Can be null | Yes (nullptr) |
No — must be initialized to a real object |
Needs dereferencing (*) |
Yes | No — used exactly like the original variable |
Supports arithmetic (p++) |
Yes | No |
| What it is under the hood | A variable holding a memory address | Almost always compiled as a pointer internally, but the language guarantees it's never null and never reseated |
int a = 10;
int* ptr = &a; // ptr holds a's memory address
int& ref = a; // ref IS a, just an alias
*ptr = 20; // changes a to 20, via dereferencing
ref = 30; // changes a to 30, directly — no dereference syntax needed
2.4 RAII — The Idea That Makes C++ Safe(r)
Resource Acquisition Is Initialization is the core C++ idiom: tie a resource's lifetime (memory, file handles, network sockets, locks) to an object's lifetime. Acquire the resource in the constructor, release it in the destructor. Because the stack guarantees destructors run when an object goes out of scope — even during an exception unwind — resources get cleaned up automatically, deterministically, with zero garbage collector.
class FileHandle {
FILE* f;
public:
FileHandle(const char* path) { f = fopen(path, "r"); } // acquire
~FileHandle() { if (f) fclose(f); } // release — always runs
};
void process() {
FileHandle fh("data.txt");
// ... do work, maybe throw an exception ...
} // fh's destructor runs HERE regardless of how the function exits — file always closed
This single idiom is why modern C++ code rarely needs manual delete at all — see smart pointers in Part 6.
Part 3 — Object-Oriented C++, Down to the Bytes
3.1 What a class Actually Is in Memory
A class instance in memory is just its data members laid out contiguously (plus padding for alignment). Member functions are not stored per-object — there's exactly one copy of each function's machine code, shared by every instance, and it receives a hidden this pointer to know which object's data to operate on.
class Point {
int x, y;
public:
void print() { std::cout << x << "," << y; }
};
// sizeof(Point) == 8 bytes (two ints) — print() adds ZERO bytes per instance
obj.print() is really compiler sugar for Point::print(&obj) — this is just an implicit first parameter.
3.2 Constructors, Destructors, and the "Rule of Five"
Every class that manages a resource should define (or explicitly default/delete) five special functions:
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : data(new int[n]), size(n) {} // constructor
~Buffer() { delete[] data; } // destructor
Buffer(const Buffer& other) // copy constructor
: data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
}
Buffer& operator=(const Buffer& other) { /* copy-assign */ return *this; }
Buffer(Buffer&& other) noexcept // move constructor
: data(other.data), size(other.size) {
other.data = nullptr; // steal the pointer, leave source empty
}
Buffer& operator=(Buffer&& other) noexcept { /* move-assign */ return *this; }
};
If you don't declare these, the compiler generates default versions — and the default copy constructor does a shallow copy (copies the pointer value, not the data it points to). Two Buffer objects would then both think they own the same heap memory, and both destructors would try to delete it — a double-free crash. This is the single most common source of C++ memory bugs in student code.
3.3 Inheritance and Virtual Functions — the vtable
When a class has at least one virtual function, the compiler adds a hidden pointer to every instance — the vptr — pointing to a virtual table (vtable): an array of function pointers, one per virtual function, specific to that class.
class Animal {
public:
virtual void speak() { std::cout << "..."; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof"; }
};
Animal* a = new Dog();
a->speak(); // prints "Woof" — NOT "..."
How a->speak() resolves at runtime:
- Follow
a's hidden vptr toDog's vtable (notAnimal's — vptr was set toDog's vtable when theDogconstructor ran). - Look up the
speakslot in that vtable → it points toDog::speak. - Call that function.
This indirection — one extra pointer dereference — is the entire mechanism behind runtime polymorphism. Without virtual, the compiler would resolve a->speak() at compile time based on a's declared type (Animal*), calling Animal::speak regardless of the actual object — this is why forgetting virtual on a base class destructor is a classic bug: deleting a Dog* through an Animal* pointer without a virtual destructor only runs ~Animal(), leaking Dog's resources.
class Animal {
public:
virtual ~Animal() {} // ALWAYS virtual if the class will be inherited from and deleted polymorphically
};
3.4 Abstract Classes and Interfaces
A pure virtual function (virtual void f() = 0;) has no implementation in that class, making the class abstract — it can't be instantiated, only inherited from. This is C++'s equivalent of an "interface."
Part 4 — Templates: Compile-Time Code Generation
Templates let you write one function/class definition that the compiler generates real, type-specific code for at compile time — this is fundamentally different from generics in Java/C# (which are largely runtime type-erasure tricks).
template <typename T>
T maxOf(T a, T b) {
return (a > b) ? a : b;
}
maxOf(3, 5); // compiler generates a real int version: maxOf<int>
maxOf(3.1, 5.9); // compiler generates a SEPARATE real double version: maxOf<double>
This process — template instantiation — happens entirely at compile time. Each distinct type you call maxOf with produces its own compiled function, specialized and optimized for that type, with zero runtime overhead compared to hand-writing each version yourself. The cost: larger binaries (code bloat) if you instantiate with many types, and famously cryptic compiler errors when something goes wrong, since the error points into generated code you never wrote by hand.
This is also the mechanism behind the entire Standard Template Library.
Part 5 — The Standard Template Library (STL)
The STL is built on three cooperating pieces:
5.1 Containers
Data structures — vector, list, deque, map, unordered_map, set, stack, queue. Know their underlying implementation, because it drives every complexity guarantee:
| Container | Underlying structure | Key trade-off |
|---|---|---|
vector |
Contiguous dynamic array | O(1) random access; O(n) insert/erase in the middle; amortized O(1) push_back |
list |
Doubly linked list | O(1) insert/erase anywhere (with an iterator); O(n) random access |
map |
Red-black tree (balanced BST) | O(log n) everything, always sorted by key |
unordered_map |
Hash table | O(1) average lookup/insert; O(n) worst case; no ordering |
deque |
Array of fixed-size blocks | O(1) push/pop at both ends |
std::vector<int> v = {1, 2, 3};
v.push_back(4); // if capacity is exceeded, vector ALLOCATES A NEW, larger buffer,
// copies/moves every existing element over, then frees the old buffer
// — this is why push_back is "amortized" O(1), not strictly O(1)
5.2 Iterators
A uniform way to traverse any container, abstracting away its internal structure — an iterator behaves like a generalized pointer (*it dereferences, ++it advances) regardless of whether the underlying container is an array or a tree.
5.3 Algorithms
Generic functions (sort, find, accumulate, transform...) that operate purely through iterators, so the same std::sort call works on a vector, a deque, or a raw array — this is templates + iterators working together.
std::vector<int> v = {5, 3, 1, 4};
std::sort(v.begin(), v.end()); // introsort: quicksort, falls back to heapsort if recursion gets too deep, insertion sort for small partitions
Part 6 — Modern C++: Smart Pointers and Move Semantics
6.1 Smart Pointers — RAII Applied to Pointers
Modern C++ (C++11 onward) strongly discourages raw new/delete in application code. Smart pointers wrap a raw pointer in a RAII object:
-
unique_ptr<T>— sole ownership. Cannot be copied, only moved. Zero overhead vs. a raw pointer. Deletes its object automatically when it goes out of scope. -
shared_ptr<T>— shared ownership via reference counting. Every copy increments an atomic counter; every destruction decrements it; the object is deleted when the count hits zero. -
weak_ptr<T>— a non-owning observer of ashared_ptr's object, used to break reference cycles (twoshared_ptrs pointing at each other would otherwise never hit zero and leak forever).
std::unique_ptr<Buffer> b1 = std::make_unique<Buffer>(100);
// b1 automatically deletes its Buffer when it goes out of scope — no manual delete, ever
std::shared_ptr<Buffer> s1 = std::make_shared<Buffer>(100);
std::shared_ptr<Buffer> s2 = s1; // refcount now 2
// object freed only when the LAST shared_ptr referencing it is destroyed
6.2 Move Semantics and Rvalue References
Before C++11, copying a large object (like a vector with a million elements) always meant allocating new memory and copying every element — even when the source object was a temporary about to be destroyed anyway. Move semantics let you steal the source's internal pointers instead of copying data, then leave the source in a valid-but-empty state.
std::vector<int> makeBigVector() {
std::vector<int> v(1'000'000, 42);
return v; // modern compilers move (or elide entirely) this — no 1M-element copy
}
std::vector<int> a = makeBigVector(); // just pointer-swapping, not copying
An rvalue reference (T&&) is how the compiler lets you write a function overload specifically for "this argument is a temporary/about-to-die object, so it's safe to cannibalize it" — that's exactly what the move constructor in section 3.2 does.
Part 7 — Exceptions and Error Handling
C++ exceptions unwind the stack when thrown — and RAII means every object on that stack gets its destructor called during unwinding, so resources are released even mid-exception, without explicit catch/cleanup blocks everywhere.
void risky() {
FileHandle fh("data.txt"); // acquired
throw std::runtime_error("failure");
// fh's destructor STILL runs during stack unwinding — file still gets closed
}
int main() {
try {
risky();
} catch (const std::exception& e) {
std::cerr << e.what();
}
}
Exceptions have real runtime cost only when actually thrown (the "zero-cost" model used by most modern compilers) — a try block with no exception thrown costs essentially nothing at runtime, which is why exceptions are preferred over manual error-code checking in performance-sensitive C++ when errors are the exceptional case.
Part 8 — Compilation Details Every Professional Should Know
- Undefined Behavior (UB): things like signed integer overflow, reading uninitialized memory, or out-of-bounds array access don't have a guaranteed result — the compiler is legally allowed to assume they never happen, and can optimize your code in surprising (even "impossible-looking") ways if they do. This is not the same as a runtime crash — UB can silently produce wrong results.
-
constcorrectness: marking a methodconst(e.g.,int getX() const) is a compile-time promise it won't modify the object, enforced by the compiler, catching entire classes of bugs before runtime. -
Inline and optimization: the compiler, not you, ultimately decides what to inline —
inlinetoday is more about allowing multiple translation units to define the same function (e.g., in headers) than a performance directive. -
Name mangling: because C++ supports overloading, the compiler encodes parameter types into each function's symbol name internally — this is why
extern "C"exists, to disable mangling when interfacing with C code/libraries.
Part 9 — Putting It All Together: One Program, Traced
#include <iostream>
#include <memory>
class Counter {
int count;
public:
Counter() : count(0) {}
void increment() { count++; }
int get() const { return count; }
};
int main() {
auto c = std::make_unique<Counter>(); // heap alloc, RAII-managed
for (int i = 0; i < 3; ++i) {
c->increment(); // stack frame for increment() pushed/popped each iteration
}
std::cout << c->get() << std::endl;
} // c goes out of scope -> unique_ptr's destructor runs -> Counter freed automatically
What happens, stage by stage:
-
Preprocessing expands
<iostream>and<memory>. -
Compilation type-checks everything, generates machine code, leaves
std::cout's implementation as an unresolved symbol. - Linking resolves that symbol against the standard library.
-
Loading: OS creates the process, sets up stack and heap, jumps into
main. -
main's stack frame is pushed. -
make_unique<Counter>()allocates aCounteron the heap, wraps it in aunique_ptron the stack. - Each loop iteration pushes/pops
increment()'s own tiny stack frame. -
get()is called — beingconst, the compiler guarantees it can't mutatecount. - When
mainreturns,c's destructor (part ofunique_ptr) runs automatically — RAII — freeing the heap-allocatedCounter, nodeletewritten anywhere. -
main's stack frame pops, the process exits, OS reclaims all its memory.
Why This Matters for Students vs. Professionals
For students: this is exactly the mental model examiners and interviewers are testing when they ask "what's the difference between stack and heap," "what does virtual do," or "why is the Rule of Five needed" — these aren't trivia, they're checks that you understand what your code actually does to memory.
For professionals: this model is what you're debugging against every time you chase a segfault, a memory leak in a long-running service, or unexpected performance cliffs from cache-unfriendly data structures. Tools like Valgrind, AddressSanitizer, and perf all report findings in exactly these terms — stack vs. heap, vtable dispatch, allocation patterns.
Next in this series: a deep dive into concurrency in C++ — std::thread, mutexes, atomics, and the memory model that makes multi-threaded C++ notoriously hard to get right. Follow along if that'd be useful.
Top comments (0)