Pointers vs References Deep Dive
A pointer is a house address written on a sticky note (takes 8 bytes; you can erase it or write 0x0). A reference is a nickname for the house itself (takes 0 bytes; you can never redirect a nickname to a different house).
In well-defined C++, a reference can never be null (int& r = *static_cast<int*>(nullptr); is immediate Undefined Behavior at the moment the null pointer is dereferenced). In API design, this is a contract: take parameters by reference (const T& or T&) when the caller must supply a valid object, and take a pointer (const T* or T*) only when 'no object' (nullptr) is a valid optional input.
A pointer on a 64-bit platform always takes 8 bytes (or 16 bytes for certain member function pointers). A reference is an alias in the C++ standard (sizeof(r) == sizeof(target)). At the machine level, if the reference is local to a function, the compiler usually optimizes it away completely (0 extra bytes). If stored as a struct/class member or passed to an external non-inlined function, the ABI stores an 8-byte address identical to a T* const pointer.
std::vector<T> requires T to be Erasable and Assignable and allocates a contiguous T* array in memory. References are not objects—they have no address of their own (int&* is illegal) and cannot be rebound after initialization. The standard workaround is std::vector<std::reference_wrapper<int>> (created via std::ref(x)), which wraps a raw int* inside a small copyable/assignable value object.
Interviewer's lens
Interviewers use pointers versus references as a litmus test: do you see variables as abstract symbols, or do you understand physical machine addresses versus compiler aliases?
- The Amateur View: Reciting textbook answers like 'a pointer can be null, but a reference cannot.'
- The Senior View: Explaining that a pointer holds a 64-bit house address in its own box of memory, while a reference is an alias—a sticky label slapped onto an existing box.
- What Evaluators Test:
- How compilers lower references to zero-cost CPU registers or hidden non-null pointers (
T* const). - Why pointer arithmetic advances by chunks of
sizeof(T)bytes rather than 1 single byte. - Why jagged 2D arrays (
int**) destroy CPU cache locality compared to contiguous flat buffers.
- How compilers lower references to zero-cost CPU registers or hidden non-null pointers (
Under the hood & memory layout
Think of memory like a vast street of houses:
- A raw pointer (
T*) is a notepad where you write down an 8-byte street address. It lives in its own box of memory, can be erased, rewritten to point to another house, or left empty (nullptr). - A reference (
T&) is a second nameplate nailed to the front door of an existing house. It is not an independent box—it is an alias.
Under the hood on a 64-bit architecture (x86_64 / ARM64):
- Raw Pointers: Always occupy 8 bytes (
sizeof(void*) == 8). They hold a 64-bit virtual address and support dereferencing (*p) and arithmetic (p + 1). - References at Compile Time: Have no distinct address of their own in the C++ abstract machine. Taking
&refyields the address of the original target, andsizeof(ref)returnssizeof(T). - Machine Code Lowering: With optimizations (
-O2), local references disappear into CPU registers at zero runtime cost. When passed across non-inlined function calls or stored inside structs, the ABI lowers them into a non-null, non-rebindable pointer (T* const).
8 bytes on 64-bit (sizeof(void*) == 8). Holds a virtual address (e.g. 0x7ffd4010) or 0x0 (nullptr). Can be reassigned (p = &other) and indexed (p + k advances by k * sizeof(T) bytes).
Alias to an existing object. Must be bound at initialization, cannot be rebound (r = y overwrites the target's value, not the binding!), and &r == &target is always true.
The CPU calculates reinterpret_cast<uintptr_t>(p) + i * sizeof(T). For int* (sizeof(int) == 4), p + 1 adds 4 bytes, not 1 byte. Subtracting two pointers q - p divides the byte distance by sizeof(T) (std::ptrdiff_t).
A naive new int*[R] + loop of new int[C] performs R + 1 separate heap allocations scattered in RAM. A contiguous new int[R * C] uses 1 heap block, keeping rows adjacent in L1/L2 cache.
Core concepts & syntax traps
Rebinding vs Value Assignment & Why Arrays of References Are Illegal
Rebinding vs Value Assignment & Why Arrays of References Are Illegal
Think of a reference as a nickname glued permanently to a person. Once tied to a variable, it cannot be unpeeled to name someone else:
- Rebinding is Impossible: Writing
r = b;does not rebindrtob. It copiesb's value into the target variablea. The address&rremains forever identical to&a. - No Independent Identity: References have no distinct address or standalone size. In standard C++, pointer-to-reference (
int&*) and arrays of references (int& arr[5]) are illegal syntax. - STL Container Rule: Because
std::vector<T>requires elements to be assignable and hold their own memory addresses,std::vector<int&>will fail to compile. - The Standard Workaround: When you need a container of references, use
std::reference_wrapper<T>(std::ref(x)from<functional>), which wraps a raw pointerT*inside a copyable value object.
int a = 10, b = 99;
int& r = a;
r = b; // Does NOT rebind r to b! Overwrites a with 99.
// a is now 99, b is 99, and &r is still &a.
// Need a vector of references? Use std::reference_wrapper:
std::vector<std::reference_wrapper<int>> refs = {a, b};
refs[0].get() = 500; // mutates 'a' directly!Use a reference when 'null' and rebinding are impossible; use a pointer (or std::reference_wrapper) when you must rebind or store in a container.
nullptr vs NULL vs 0 (Why NULL Causes Overload Bugs)
nullptr vs NULL vs 0 (Why NULL Causes Overload Bugs)
In legacy C, NULL is simply the integer 0 (or 0L). This creates a dangerous trap when functions are overloaded:
- The Overload Ambiguity Trap: If you declare
void route(int)andvoid route(int*), callingroute(NULL)passes an integer0. The compiler selectsroute(int)—or halts with an ambiguous call error. - The Modern C++ Solution: C++11 introduced
nullptr, a dedicated keyword of typestd::nullptr_t. - Strict Type Safety:
nullptrconverts implicitly to any raw pointer (T*) or pointer-to-member, but never converts to an integer (int). It guarantees you call the pointer overload every time.
void route(int id); // Overload 1: integer ID
void route(int* payload); // Overload 2: pointer
// route(NULL); // BUG: calls route(int) because NULL is 0!
route(nullptr); // Calls route(int*) unambiguously (typeof nullptr is std::nullptr_t)Never write NULL or 0 for pointers in modern C++; always use nullptr.
Pointer Arithmetic, Array Decay & std::ptrdiff_t
Pointer Arithmetic, Array Decay & std::ptrdiff_t
Pointers do not advance byte-by-byte; they jump in strides equal to the size of the underlying type:
- Stride by Type Size: For an integer pointer (
int* p), writingp + 1does not add 1 byte. The CPU calculatesaddress + 1 * sizeof(int)(an advance of 4 bytes). - Array Decay: When passed into a function, a C-style array (
int arr[5]) loses its size metadata and decays into a raw pointer to its first element (&arr[0]). Inside the function,sizeof(p)is 8 bytes (the pointer itself), not 20 bytes. - Pointer Subtraction (
std::ptrdiff_t): Subtracting two pointers within the same array (q - p) computes the number of elements between them, not the raw byte delta. The result is a signed integer of typestd::ptrdiff_t. - Boundary Invariant: You may legally point to one-past-the-end (
arr + 5) to form an STL-style half-open range[begin, end), but dereferencing that address is immediate Undefined Behavior.
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // decays to &arr[0]
int* onePast = arr + 5; // LEGAL to form and compare, UB to dereference (*onePast)
std::ptrdiff_t n = onePast - p; // 5 elements (20 bytes / sizeof(int))Prefer std::span<T> over raw (T* ptr, size_t len) pairs to prevent array decay bugs.
Multi-Level Pointers (int**) & Modifying a Caller's Pointer
Multi-Level Pointers (int**) & Modifying a Caller's Pointer
A classic interview trap is asking you to allocate memory or advance a pointer inside a helper function:
- The By-Value Trap: If a function takes
void alloc(int* p), the pointer variable itself is passed by value (copied into a CPU register). Writingp = new int(42);inside the helper only updates the local copy; the caller's pointer remainsnullptr. - The Double Pointer Fix (
int** pp): By passing the memory address of the caller's pointer (&ptr), the helper can dereference it (*pp = new int(42);) to overwrite the caller's address. This is the classic C-style approach. - The Idiomatic C++ Fix (
int*& rp): Taking a reference to a pointer creates an alias to the caller's pointer variable directly. Writingrp = new int(42);updates the caller without double-pointer syntax clutter.
// BUG: mutates local copy of p; caller's pointer stays nullptr!
void brokenAlloc(int* p) { p = new int(42); }
// FIX 1 (C++ style — Reference to Pointer):
void allocByRefToPtr(int*& p) { p = new int(42); }
// FIX 2 (C/Systems style — Double Pointer):
void allocByDoublePtr(int** pp) { if (pp) *pp = new int(42); }To modify where a caller's pointer points, accept T*& (idiomatic C++) or T** (C ABI compatible).
Function Pointers & Callbacks
Function Pointers & Callbacks
Functions are not just code—they reside at specific byte addresses in the executable's .text segment:
- Function Address: A function pointer stores the entry address of that compiled machine code block. Calling
fnPtr(arg)issues an indirect jump instruction (call raxin x86_64 assembly). - Zero Allocation Overhead: Unlike heavy wrapper types like
std::function, raw function pointers require zero heap allocation and add no dynamic dispatch overhead. - Modern Type Aliases: The classic C syntax
int (*fn)(int, int)is notoriously difficult to read. In modern C++, declare aliases withusing MathOp = int (*)(int, int);. - Stateless Lambdas: Any C++ lambda that captures no variables (
[]) can implicitly convert to a raw C-style function pointer, making it fully compatible with legacy C APIs.
using MathOp = int (*)(int, int);
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
MathOp op = &add; // or just 'add'
int res = op(4, 5); // 9 — indirect call via registerStateless lambdas [](int a, int b) { return a + b; } implicitly convert to raw C function pointers!
Phase 1 (15-min baseline): Pointer vs Reference Swap, Null-Safety Guards & Address Identity
In a 30-minute systems interview, do not jump straight into complex 2D contiguous memory matrices or function pointer dispatch tables. First, write this concise, ~40-line whiteboard baseline demonstrating safe pointer dereferencing with null guards, reference swapping, array decay mechanics, and address identity testing. It proves immediate mastery of machine addresses vs compiler aliases in under 12 minutes.
#include <iostream>
#include <utility>
// 1. Pointer swap requires null & self-aliasing guards
void swapByPtr(int* a, int* b) {
if (!a || !b || a == b) return;
int tmp = *a;
*a = *b;
*b = tmp;
}
// 2. Reference swap: compiler guarantees non-null binding
void swapByRef(int& a, int& b) {
if (&a == &b) return; // Address identity check
int tmp = a;
a = b;
b = tmp;
}
// 3. Pointer arithmetic & two-pointer array reversal
void reverseArray(int* begin, int* end) {
if (!begin || !end || begin >= end) return;
int* left = begin;
int* right = end - 1; // STL half-open [begin, end)
while (left < right) {
swapByRef(*left, *right);
++left;
--right;
}
}
void inspectAddresses() {
int x = 10;
int& ref = x;
int* ptr = &x;
std::cout << "(&ref == &x): " << std::boolalpha << (&ref == &x) << '\n';
std::cout << "(ptr == &x): " << std::boolalpha << (ptr == &x) << '\n';
std::cout << "(*ptr == x): " << std::boolalpha << (*ptr == x) << '\n';
std::cout << "(&ptr != &x): " << std::boolalpha << ((void*)&ptr != (void*)&x) << '\n';
}
int main() {
std::cout << "--- Address Inspection ---\n";
inspectAddresses();
std::cout << "\n--- Pointer Swap ---\n";
int a = 42, b = 99;
std::cout << "Before swapByPtr: a=" << a << ", b=" << b << '\n';
swapByPtr(&a, &b);
std::cout << "After swapByPtr: a=" << a << ", b=" << b << '\n';
swapByPtr(&a, nullptr); // Null guard test: no-op, no crash
std::cout << "After swapByPtr with nullptr: a=" << a << ", b=" << b << '\n';
std::cout << "\n--- Reference Swap ---\n";
swapByRef(a, b);
std::cout << "After swapByRef: a=" << a << ", b=" << b << '\n';
std::cout << "\n--- Array Reversal via Pointer Arithmetic ---\n";
int arr[] = {1, 2, 3, 4, 5};
std::cout << "Before reversal: ";
for (int v : arr) std::cout << v << ' ';
std::cout << '\n';
reverseArray(arr, arr + 5);
std::cout << "After reversal: ";
for (int v : arr) std::cout << v << ' ';
std::cout << '\n';
return 0;
}--- Address Inspection --- (&ref == &x): true (ptr == &x): true (*ptr == x): true (&ptr != &x): true --- Pointer Swap --- Before swapByPtr: a=42, b=99 After swapByPtr: a=99, b=42 After swapByPtr with nullptr: a=99, b=42 --- Reference Swap --- After swapByRef: a=42, b=99 --- Array Reversal via Pointer Arithmetic --- Before reversal: 1 2 3 4 5 After reversal: 5 4 3 2 1
Array Decay Loses Size Bounds: When passing
int* begin, int* end, the caller must calculate bounds correctly; raw pointers decay without length metadata, risking buffer overruns ifendis miscalculated.Reference Rebinding Confusion: Writing
ref = y;inswapByRefdoes NOT rebindreftoy; it mutates the storage ofxthrough the alias. References are permanently bound at initialization.Null vs Dereference Order in Guards: In pointer guards, testing
*p != 0 && p != nullptrcauses an instant segmentation fault because*pevaluates before the null check. Short-circuit logic mandatesp && *p != 0.Passing Pointer by Value for Reallocation: A helper like
void alloc(int* p)copies the 8-byte address into a register; overwritingp = new intinside the helper leaves the caller's pointer dangling or null.
"This baseline demonstrates machine-level address identity, safe pointer dereferencing, and reference parameter passing. However, in low-latency systems, using raw pointer-to-pointer structures like int** for multi-dimensional data causes R+1 separate heap allocations and cache-line misses across rows. If you'd like, I can now pivot to Phase 2 and build a production contiguous 2D matrix that delivers 100% cache locality while maintaining clean matrix[r][c] bracket indexing."
Phase 2 (Production C++23): Pointer vs Reference Swap, In-Place Array Reversal & Function Pointer Callbacks
Implement: (1) swapByPtr(int*, int*) with null/self guards and swapByRef(int&, int&), (2) reverseWithPtrs(int* begin, int* end) using two-pointer arithmetic, and (3) a higher-order applyTransform function using a raw function pointer callback.
- 1.
In
swapByPtr(int* a, int* b), guard against!a || !b || a == bbefore dereferencing; inswapByRef(int& a, int& b), references are guaranteed non-null by the caller. - 2.
In
reverseWithPtrs(int* begin, int* end), follow the STL half-open range convention[begin, end). Initializeleft = beginandright = end - 1, swapping*leftand*rightwhileleft < right. - 3.
Verify pointer distance vs byte distance:
(arr + 5) - arrequals5elements, whereas casting toconst char*reveals the raw byte offset5 * sizeof(int) = 20. - 4.
Define
using TransformFn = int (*)(int);and passdoubleValandsquareValintoapplyTransformover astd::span<int>.
#include <cstdint>
#include <print>
#include <span>
// ============================================================================
// 1. POINTER SWAP vs REFERENCE SWAP
// Pointers can be null or identical; references are guaranteed non-null aliases.
// ============================================================================
// Pointer swap: MUST check for nullptr before dereferencing (*a), otherwise immediate UB!
void swapByPtr(int* a, int* b) noexcept {
// Guard 1: Ignore null pointers safely
// Guard 2: If both pointers point to the exact same address (a == b), nothing to do
if (!a || !b || a == b) return;
int tmp = *a; // Step 1: Read value from memory address 'a' into a temporary register
*a = *b; // Step 2: Overwrite memory address 'a' with the value from 'b'
*b = tmp; // Step 3: Overwrite memory address 'b' with the saved temporary value
}
// Reference swap: Guaranteed valid by the compiler — no null checks needed, zero overhead!
void swapByRef(int& a, int& b) noexcept {
int tmp = a; // Step 1: Read value from 'a' (directly compiled into registers)
a = b; // Step 2: Assign value of 'b' into 'a'
b = tmp; // Step 3: Assign saved value into 'b'
}
// ============================================================================
// 2. POINTER ARITHMETIC IN-PLACE REVERSAL
// Demonstrates that `ptr + 1` does NOT advance by 1 byte; it advances by sizeof(T)!
// ============================================================================
void reverseWithPtrs(int* begin, int* end) noexcept {
if (!begin || !end) return;
int* left = begin; // Points to the very first element (index 0)
int* right = end - 1; // Points to the very last element (index N - 1)
// Two-pointer loop meeting in the middle:
while (left < right) {
swapByRef(*left, *right); // Swap the values at the two boundary pointers
++left; // Advances address forward by exactly sizeof(int) = 4 bytes!
--right; // Steps address backward by exactly sizeof(int) = 4 bytes!
}
}
// ============================================================================
// 3. FUNCTION POINTER CALLBACKS
// Modern C++ passes executable code addresses using 8-byte function pointers.
// ============================================================================
using TransformFn = int (*)(int); // Type alias: pointer to a function taking int and returning int
int doubleVal(int x) { return x * 2; }
int squareVal(int x) { return x * x; }
// Applies the given transformation function to every element in the contiguous span
void applyTransform(std::span<int> data, TransformFn fn) {
if (!fn) return; // Guard: protect against null function pointer call (prevents SIGSEGV)
for (int& v : data) {
v = fn(v); // Indirect call: CPU jumps to the address of 'fn'
}
}
int main() {
// Test 1: Swapping via pointers vs references
int x = 10, y = 25;
swapByPtr(&x, &y); // Must pass explicit memory addresses using & operator
std::println("After swapByPtr: x={}, y={}", x, y);
swapByRef(x, y); // Passed directly as variables; compiler treats them as aliases
std::println("After swapByRef: x={}, y={}", x, y);
// Test 2: Pointer distance & byte arithmetic
int arr[] = {1, 2, 3, 4, 5};
std::ptrdiff_t dist = (arr + 5) - arr; // Subtracting pointers yields element count (5), NOT bytes!
std::println("Pointer distance (elements): {}, byte delta: {}",
dist,
reinterpret_cast<const char*>(arr + 5) - reinterpret_cast<const char*>(arr));
// Test 3: Array reversal using two pointers
reverseWithPtrs(arr, arr + 5);
std::println("Reversed array: {} {} {} {} {}", arr[0], arr[1], arr[2], arr[3], arr[4]);
// Test 4: Dynamic dispatch via function pointer
applyTransform(arr, doubleVal);
std::println("After doubleVal callback: {} {} {} {} {}", arr[0], arr[1], arr[2], arr[3], arr[4]);
applyTransform(arr, squareVal);
std::println("After squareVal callback: {} {} {} {} {}", arr[0], arr[1], arr[2], arr[3], arr[4]);
return 0;
}After swapByPtr: x=25, y=10 After swapByRef: x=10, y=25 Pointer distance (elements): 5, byte delta: 20 Reversed array: 5 4 3 2 1 After doubleVal callback: 10 8 6 4 2 After squareVal callback: 100 64 36 16 4
swapByPtr/swapByRef: O(1) time, O(1) space. At-O2, both compile to identical register exchanges.reverseWithPtrs: O(N) time, O(1) auxiliary space (N/2 swaps).applyTransform: O(N) time, O(1) space, zero heap allocation (unlikestd::functionwith large captures).
Interviewer cross-questioning & viva playbook
"Consider int a = 20; int& b = a; auto c = b; — What is the exact type of c? Now what if we write auto c = std::move(b); followed by c = 99;? Does b move to c, does a change, and why does changing c leave a at 20?"
Candidates often assume auto c = b; deduces int& (a reference), or believe std::move(b) 'steals' the resource from b or empties a.
autoStrips References and Top-Level Const: By language design (following template argument deduction rules), plainautodeduces by value. In bothauto c = b;andauto c = std::move(b);, the reference qualifier&is completely discarded. The deduced type ofcis a standaloneintallocated at a distinct 4-byte stack address (e.g.0x7ffd04), completely independent ofa(at0x7ffd00). To retain a reference, one must explicitly writeauto& c = b;.std::move(b)Performs Zero Runtime Work:std::moveis merely a compile-time cast:static_cast<std::remove_reference_t<decltype(b)>&&>(b), which yields an rvalue referenceint&&pointing toa.- Moving Primitives is a 4-Byte Copy: Primitive scalar types (
int,double, raw pointers) do not manage dynamic heap buffers or ownership handles. For fundamental types, move construction is identical to copy construction: the CPU simply executes a 4-byte memory/register copy (mov eax, [rbp-4]; mov [rbp-8], eax).aremains20. - Mutation Independence: Because
cis an independent stack variable, executingc = 99;writes strictly toc's stack slot (0x7ffd04).aandbat0x7ffd00remain unchanged at20.
int a = 20; // Stack @ 0x7ffd00 = 20
int& b = a; // Alias to 0x7ffd00
auto c1 = b; // c1 is 'int' @ 0x7ffd04 (copies 20)
auto c2 = std::move(b);// c2 is 'int' @ 0x7ffd08: primitive move is just 4-byte copy!
c2 = 99; // Writes 99 to 0x7ffd08
// a is still 20, c2 is 99!
auto& refD = b; // refD is 'int&' aliasing 0x7ffd00
refD = 77; // Now 'a' is 77!"Why does declaring std::vector<int&> trigger a compilation error? Walk me through the exact language specifications violated, and explain how std::reference_wrapper<int> solves this."
Assuming the vector cannot store references because references 'take 0 bytes' or guessing that the error only occurs at runtime when resizing or reallocating memory.
"Does a reference occupy 0 bytes or 8 bytes in memory? Show how the compiler lowers a reference in local scope versus across non-inlined function calls or struct members in x86-64 assembly."
Dogmatically answering '0 bytes' because sizeof(ref) == sizeof(T) in the standard, or claiming references are purely high-level syntax with no machine representation.
"Why does void alloc(int* p) { p = new int(42); } fail to return allocated memory to the caller, and what are the two ways to fix it? Explain the stack frame mechanics."
Assuming that because p is a pointer, modifying p automatically reflects back to the caller's pointer variable.
Scenario: Dynamic 2D Matrix in C++: Why int** Is Slow and How to Allocate a Contiguous 2D Block
The interviewer asks: "Write a function to dynamically allocate a 2D rows x cols matrix so the caller can index mat[r][c]. Then tell me what is wrong with the classic new int*[rows] loop in a low-latency system, and rewrite it so the entire 2D matrix lives in a single contiguous memory block while still supporting mat[r][c] syntax!"
The naive new int*[rows] loop calls the OS/heap allocator rows + 1 times, scattering rows across non-adjacent heap addresses (causing CPU cache misses on row transitions) and requiring a loop to delete[]. We fix this by allocating one flat array new int[rows * cols] for all elements, and wiring up rowPtrs_[r] = data_ + r * cols so mat[r][c] still works with 100% contiguous cache locality and just two delete[] calls inside an RAII destructor.
#include <cstddef>
#include <print>
// ============================================================================
// SCENARIO: JAGGED 2D ARRAY vs CONTIGUOUS 2D MATRIX
// Demonstrates why jagged arrays thrash CPU caches and how contiguous layout fixes it.
// ============================================================================
// ----------------------------------------------------------------------------
// Approach A: Jagged 2D Array (int**)
// Requires R + 1 separate heap allocations; rows are scattered unpredictably in RAM!
// ----------------------------------------------------------------------------
int** allocateJagged2D(std::size_t rows, std::size_t cols) {
// Allocation 1: Array of pointers to hold the starting address of each row
int** mat = new int*[rows];
// Allocations 2 to R+1: Allocate each individual row separately on the heap
for (std::size_t r = 0; r < rows; ++r) {
mat[r] = new int[cols]{}; // Zero-initialized memory for each row
}
return mat;
}
// Teardown: Every single row must be deleted before deleting the outer pointer array!
void freeJagged2D(int** mat, std::size_t rows) noexcept {
if (!mat) return;
for (std::size_t r = 0; r < rows; ++r) {
delete[] mat[r]; // Step 1: Free each scattered row buffer
}
delete[] mat; // Step 2: Free the array of row pointers
}
// ----------------------------------------------------------------------------
// Approach B: Contiguous 2D Matrix (Production-Grade High Performance)
// Allocates ALL cells in ONE single flat heap block for 100% L1/L2 cache prefetching!
// ----------------------------------------------------------------------------
class ContiguousMatrix {
std::size_t rows_;
std::size_t cols_;
int* data_; // 1 single contiguous heap block of (rows * cols) integers
int** rowPtrs_; // Lightweight pointer table indexing into data_ so mat[r][c] syntax works!
public:
ContiguousMatrix(std::size_t rows, std::size_t cols)
: rows_(rows), cols_(cols),
data_(new int[rows * cols]{}), // 1 flat allocation for all elements
rowPtrs_(new int*[rows]) { // 1 pointer table for O(1) row indexing
// Point each row pointer to its exact contiguous slice inside data_
for (std::size_t r = 0; r < rows_; ++r) {
rowPtrs_[r] = data_ + (r * cols_); // Exact byte offset calculation
}
}
// Clean RAII Destructor: 2 delete calls total (vs R+1 deletes in jagged array!)
~ContiguousMatrix() noexcept {
delete[] rowPtrs_; // Free the pointer lookup table
delete[] data_; // Free the single continuous element block
}
// Rule of 5: Prevent accidental double-frees by disabling raw copies
ContiguousMatrix(const ContiguousMatrix&) = delete;
ContiguousMatrix& operator=(const ContiguousMatrix&) = delete;
// Subscript operators allowing native mat[r][c] bracket syntax
int* operator[](std::size_t r) noexcept { return rowPtrs_[r]; }
const int* operator[](std::size_t r) const noexcept { return rowPtrs_[r]; }
// Physical proof: distance from row 1 start to row 0 start equals cols exactly
std::ptrdiff_t rowStrideElements() const noexcept {
return rowPtrs_[1] - rowPtrs_[0];
}
};
int main() {
// Test 1: Jagged 2D array
int** jagged = allocateJagged2D(3, 4);
jagged[1][2] = 42;
std::println("Jagged [1][2] = {}", jagged[1][2]);
freeJagged2D(jagged, 3);
// Test 2: Contiguous matrix (Hardware cache friendly)
ContiguousMatrix mat(3, 4);
int val = 1;
for (std::size_t r = 0; r < 3; ++r) {
for (std::size_t c = 0; c < 4; ++c) {
mat[r][c] = val++;
}
}
std::println("Contiguous [0][3] = {}, [1][0] = {}", mat[0][3], mat[1][0]);
std::println("Pointer delta (&mat[1][0] - &mat[0][0]) = {} ints (strictly contiguous!)",
mat.rowStrideElements());
return 0;
}Jagged [1][2] = 42 Contiguous [0][3] = 4, [1][0] = 5 Pointer delta (&mat[1][0] - &mat[0][0]) = 4 ints (strictly contiguous!)
Common C++ mistakes & UB traps
int& badRef() {
int local = 42;
return local; // UB: stack frame is popped on return!
}int goodVal() {
int local = 42;
return local; // Safe: returned by value in CPU register
}When badRef() returns, its stack frame is reclaimed immediately. The returned reference (or pointer) dangles into dead stack memory that the next function call will overwrite.
void clear(int arr[10]) {
// BUG: sizeof(arr) is sizeof(int*) == 8 on 64-bit, NOT 40!
std::memset(arr, 0, sizeof(arr));
}void clear(std::span<int> arr) {
std::fill(arr.begin(), arr.end(), 0);
}Function parameter int arr[10] is silently rewritten by the C++ compiler to int* arr. Always pass std::span<T>, std::array<T, N>&, or an explicit element count.
if (*ptr > 0 && ptr != nullptr) { ... }if (ptr != nullptr && *ptr > 0) { ... }&& evaluates strictly left-to-right. Dereferencing *ptr on the left crashes with a segmentation fault before ptr != nullptr is ever checked.
"What does this print / Why does this crash?"
int a[4] = {10, 20, 30, 40};
int* p = a;
int* q = reinterpret_cast<int*>(reinterpret_cast<char*>(p) + sizeof(int) * 2);
std::println("{} {}", *(p + 1), *q);int x = 5, y = 12;
int& r = x;
int* p = &r;
r = y;
y = 99;
std::println("x={} y={} *p={}", x, y, *p);Rapid-fire interview Q&A
Can a reference ever be null in C++, and how does that affect API design?
How much memory does a reference take compared to a pointer?
Why can't you create a std::vector<int&>, and what is the standard workaround?
What is the difference between void* in C vs C++, and why can't you do pointer arithmetic on void*?
When should you pass an argument by value (T), by const reference (const T&), by non-const reference (T&), or by pointer (T*)?