Recently, I did a mock interview with my good friend and realised that there were a lot of fundamentals that I hadn't fully ingrained, along with some gaps in my C++ knowledge. Because of that, I will be starting this blog as a sort of "starting from scratch" reference.
This is an interactive list where I will be collecting, categorising, and breaking down topics by keeping definitions and code blocks tucked inside dropdown lists. Use the list below to test your understanding!
Topic 1: Memory & Basics
Subtopic: Pointers vs. References
A pointer is a distinct object in memory that stores the address of another variable and can be reassigned or set to nullptr. A reference is a pure symbol-table alias for an existing object, must be initialised when created, and cannot be null or reseated.
Under the hood, the C++ compiler usually implements references as const pointers (T* const). However, local aliases are often completely optimised away in assembly so that they take up **zero bytes of memory**, acting strictly as symbol-table aliases!
| Property | Pointer (T*) | Reference (T&) |
|---|---|---|
| What it is | A distinct object in memory storing an address. | A pure alias for an existing object. |
| Address | Has its own unique address (&ptr != &x). |
Shares the same address as target (&ref == &x). |
| Nullability | Can be nullptr. |
Cannot be null. |
// Pointer int a = 10; int* ptr = &a; // Pointer stores the address of a *ptr = 20; // Modify value through dereferencing ptr = nullptr; // Pointers can be null // Reference int b = 30; int& ref = b; // ref is an alias to b ref = 40; // Directly modifies b // int& ref2; // Error! Must be initialised
Subtopic: Stack vs. Heap Allocation
Stack allocation is automatic, very fast, and managed by the operating system CPU. Objects created here are cleaned up when going out of scope. Heap allocation (dynamic) is manual using new/delete, slower, but allows data to survive scopes (stored until explicitly freed, or the program ends).
// Stack Allocation (automatic lifetime) void func() { int x = 42; // Allocated on the stack } // x is automatically deallocated here // Heap Allocation (manual lifetime) void func2() { int* ptr = new int(42); // Allocated on the heap delete ptr; // Clean up to prevent memory leaks }
Subtopic: Value Categories (lvalue, rvalue, xvalue)
In C++, every expression has a type and belongs to exactly one value category. These categories determine how expressions are evaluated and whether they can be moved. Bjarne Stroustrup and the standards committee generalised value categories in C++11 to fit move semantics:
- Lvalue (Locator Value): An expression that points to a specific, identifiable location in memory (an addressable object). It has identity.
- Rvalue (Read Value): A temporary computed value that can be read but doesn't have a persistent address. It is expiring or transient.
┌─────────────────────────┐
│ Expression │
└────────────┬────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ glvalue │ │ rvalue │
│(Generalized Lval)│ │ (Moveable Val) │
└─────────┬────────┘ └─────────┬────────┘
│ │
┌─────┴──────────────┬────────────────┴─────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ lvalue │ │ xvalue │ │ prvalue │
│ (Locator Value) │ │ (eXpiring Value) │ │ (Pure Rvalue) │
│ │ │ │ │ │
│ Identity: YES │ │ Identity: YES │ │ Identity: NO │
│ Moveable: NO │ │ Moveable: YES │ │ Moveable: YES │
└──────────────────┘ └──────────────────┘ └──────────────────┘
- glvalue (generalised lvalue): Has identity. (lvalue + xvalue).
- lvalue (left value): Has identity, but cannot be moved. (e.g. named variables like
int x). - xvalue (expiring value): Has identity, and can be moved. (e.g. results of casting via
std::move(x)). - prvalue (pure rvalue): No identity, and can be moved. (e.g. literals like
42or temporaries). - rvalue: Can be moved. (prvalues + xvalues).
int x = 10; // 'x' is an lvalue. '10' is a prvalue. // lvalue reference int& lref = x; // OK: lvalue reference binds to lvalue 'x' // rvalue reference int&& rref1 = 42; // OK: rvalue reference binds to prvalue '42' // int&& rref2 = x; // Error! lvalue 'x' cannot bind to rvalue reference // xvalue (expiring value) int&& rref3 = std::move(x); // std::move(x) casts lvalue to an xvalue
Subtopic: Lvalue References (T&) vs. Rvalue References (T&&)
An lvalue reference (T&) can only bind to non-const lvalues. A const lvalue reference (const T&) is special: it can bind to both lvalues and rvalues, extending temporary lifetimes.
An rvalue reference (T&&) binds strictly to rvalues, signaling to the compiler that the target's heap memory can be safely "stolen" (moved).
⚠️ The Golden Paradox: "If it has a name, it is an lvalue!"
An rvalue reference variable (like int&& arg) has an identifier. Inside its scope, the expression arg is treated as an lvalue! You must wrap it in std::move(arg) to cast it back to an rvalue (xvalue) when forwarding it.
int x = 10; int& lref = x; // OK // int& lref2 = 20; // Error! Cannot bind non-const lvalue reference to rvalue const int& cref = 20; // OK! Extends lifetime of the temporary 20 int&& rref = 20; // OK! Binds strictly to rvalue 20 // int&& rref2 = x; // Error! Cannot bind rvalue reference to lvalue x void process(int&& arg) { // Inside this function, 'arg' is an LVALUE because it has a name! // To pass it as an rvalue, we must cast it: int y = std::move(arg); }
Subtopic: Universal References & Perfect Forwarding
When T&& is used in a context where type deduction occurs (such as in a function template like template <typename T> void wrapper(T&& arg); or with auto&&), it is a Universal Reference (or forwarding reference). It can bind to both lvalues and rvalues.
Its type collapses based on the C++ reference collapsing rules:
&+&→&&+&&→&&&+&→&&&+&&→&&(Only double-rvalue collapses to an Rvalue reference)
We use std::forward<T>(arg) to perfectly restore the original value category when forwarding parameters in wrapper functions, avoiding combinatorial explosions of overloads.
template <typename T> void wrapper(T&& arg) { // Perfect Forwarding: passes arg as an rvalue or lvalue // depending on what was passed to wrapper() someFunction(std::forward<T>(arg)); } int x = 10; wrapper(x); // T deduced as int&, T&& becomes int& (Lvalue Reference) wrapper(20); // T deduced as int, T&& becomes int&& (Rvalue Reference)
Topic 2: Move Semantics & The Rule of Five
Subtopic: The Core Operations (Rule of 5)
To master modern C++ (C++11 and beyond), you must develop an intuitive understanding of how objects are passed, copied, and moved in memory. If your class manages a raw resource (like a pointer to heap memory), you must implement these five operations:
- Destructor: Frees heap allocations, preventing memory leaks when objects die.
- Copy Constructor: Allocates distinct heap space and copies values, providing safe duplication.
- Copy Assignment: Safely cleans up the existing buffer first, then copies elements to avoid memory leaks.
- Move Constructor: Steals dynamic pointers instantly from temporaries (Rvalues) in
O(1)time. - Move Assignment: Overwrites current state by releasing existing heap buffers and stealing donor pointers in a single step.
Subtopic: The 4 Essential Code Patterns to Memorise
Here are the essential code patterns for resource-managing classes that you should memorise:
Pattern A: Deep Copying (Copy Constructor)
Never copy raw pointers directly (which causes double-frees). Always allocate a fresh dynamic segment:
// Deep Copying Pattern T(const T& other) : size(other.size) { data = new int[other.capacity]; for (size_t i = 0; i < other.size; i++) { data[i] = other.data[i]; } }
Pattern B: Copy-and-Swap (Copy Assignment Operator)
Guarantees strong exception safety and handles self-assignment cleanly:
// Copy-and-Swap Pattern T& operator=(const T& other) { if (this != &other) { T temp(other); // 1. Copy-construct temporary std::swap(data, temp.data); // 2. Swap internal buffers std::swap(size, temp.size); } return *this; // temp dies here, freeing the old buffer! }
Pattern C: Nulling Out (Move Constructor)
Steal the pointer and critical metadata, then null out the donor to prevent it from freeing memory inside its destructor:
// Nulling Out Pattern T(T&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; // CRITICAL: Null out donor pointer! other.size = 0; }
Pattern D: Move Assignment Guard
Safely free current heap resources before claiming the donor's:
// Move Assignment Guard Pattern T& operator=(T&& other) noexcept { if (this != &other) { delete[] data; // 1. Clean up own resource data = other.data; // 2. Steal pointer size = other.size; other.data = nullptr;// 3. Null out donor pointer! other.size = 0; } return *this; }
Topic 3: Custom Memory Allocators for Low-Latency Systems
Subtopic: Linear & Arena Allocators
In low-latency systems (such as high-frequency trading engines, operating system kernels, or high-performance game engines), calling the standard heap allocator (malloc or new) is considered far too slow due to CPU synchronisation locks and kernel context switching. C++ developers build custom memory allocators instead.
1. Bump / Linear Allocator
The simplest and fastest possible allocator. It works exactly like a call stack.
- Mechanics: Pre-allocates one large block of memory. On each allocation request, it increments (bumps) a single tracking pointer forward by the requested size and returns the previous address.
- Deallocation: You cannot free individual elements. You can only reset the entire allocator (moving the pointer back to the start) all at once.
- Complexity: Allocation:
O(1)| Deallocation:O(1)(entire block reset).
2. Arena / Monotonic Allocator
An extension of the linear allocator that handles size constraints dynamically. When the current bump block runs out of space, it dynamically allocates a new arena page and chains them together in a list.
Subtopic: Fixed & Variable-Size Allocators
1. Pool / Fixed-Size Block Allocator (Free List)
Designed for allocating objects that are all the exact same size (e.g., linked list nodes, tree vertices).
- Mechanics: Pre-allocates a chunk of memory and divides it into slots of a fixed size. It maintains a singly linked list (a Free List) linking all empty slots.
- Deallocation: Simply pushes the freed slot back onto the head of the free list.
- Complexity: Allocation:
O(1)| Deallocation:O(1)| Memory Fragmentation: 0%.
2. Slab Allocator
Pioneered by SunOS kernels, this scales fixed-size pools to accommodate different object types (e.g., file descriptors, sockets, threads), routing allocations to slabs specialised for their size and recycling structures immediately.
3. Buddy Allocator
Divides memory recursively in powers of two. When a block is freed, the allocator automatically merges adjacent buddy blocks (coalescing) back into a single larger block.
4. Free List / Variable-Size Allocator
The most general-purpose allocator. This powers standard malloc logic. It maintains a list of variable free blocks and scans them on allocation using policies like First Fit, Best Fit, or Worst Fit, requiring coalescing to manage fragmentation.
Subtopic: Allocator Comparison
| Allocator Type | Allocation | Deallocation | Variable Sizes? | Individual Free? | Fragmentation Risk |
|---|---|---|---|---|---|
| Bump / Linear | O(1) |
O(1) (All) |
Yes | No | None |
| Pool / Free List | O(1) |
O(1) |
No | Yes | None |
| Buddy Allocator | O(log N) |
O(log N) |
Yes (2^k) | Yes | Low (Internal) |
| Free List (malloc) | O(N) |
O(N) |
Yes | Yes | High |
Topic 4: Const Correctness
Subtopic: Const Pointer vs. Pointer to Const
A pointer to const means the data being pointed to cannot be altered, but the pointer address itself can. A const pointer means the address stored cannot change, but the data it points to can.
int val1 = 10; int val2 = 20; // Pointer to const (const is before *) const int* ptrToConst = &val1; // *ptrToConst = 15; // Error! Can't modify the value ptrToConst = &val2; // OK! Address can change // Const pointer (const is after *) int* const constPtr = &val1; *constPtr = 15; // OK! Can modify value // constPtr = &val2; // Error! Address cannot change
Topic 5: Object-Oriented Programming
Subtopic: Virtual Functions & Polymorphism
A virtual function is a member function in the base class that you expect to redefine in derived classes. Using a pointer or reference to the base class, C++ utilises late binding (vtable lookups at runtime) to invoke the overridden function of the derived object.
class Base { public: virtual void show() { std::cout << "Base\n"; } virtual ~Base() = default; // virtual destructor is essential! }; class Derived : public Base { public: void show() override { std::cout << "Derived\n"; } }; Base* obj = new Derived(); obj->show(); // Prints "Derived" due to runtime polymorphism delete obj;