20 topics
← Back to Quick Reference/
Topic 13
PPooiinntteerrss && RReeffeerreenncceess
Raw Pointers · References · Smart Pointers · Ownership · this · UB
C++17 · Advanced ReferencePointers & References
01Indirection in C++
Both pointers and references provide indirect access to an object — but they differ in safety guarantees and capabilities. Modern C++ adds smart pointers that express ownership intent in the type system, eliminating most reasons to use raw new and delete.
Memory model
- 1.
Stack— fast, automatic lifetime. Local variables. Limited size (~1–8 MB). - 2.
Heap— dynamic, manual (or RAII) lifetime.new/deleteor smart pointers. - 3.A pointer is a variable that holds an address — 8 bytes on 64-bit systems.
- 4.A reference is an alias — same address as the original, no extra storage.
Ownership hierarchy
- 1.
unique_ptr— sole owner. Zero overhead. Destroyed when it goes out of scope. - 2.
shared_ptr— shared ownership via reference count. Small overhead per dereference. - 3.
weak_ptr— non-owning observer. Must lock before use. Breaks cycles. - 4.Raw pointer
T*— no ownership. Use only as a non-owning observer or for C APIs.
The rule: if you write new, wrap it in make_unique immediately. If you write delete, you're probably doing it wrong.
Pointers
02int x = 10; int* ptr = &x; // ptr holds the address of x *ptr; // 10 — dereference: follow the pointer to get the value *ptr = 20; // x is now 20 — modifies through the pointer // ── Null pointer ────────────────────────────────────────────── int* p = nullptr; // ✅ C++11 null pointer (use this) // int* p = NULL; // ❌ old C macro — avoid // int* p = 0; // ❌ ambiguous — avoid if (p != nullptr) { *p; } // always null-check before dereferencing // ── Pointer to pointer ──────────────────────────────────────── int** pp = &ptr; // holds the address of a pointer **pp; // 20 — double dereference // ── Const and pointers (read right to left) ────────────────── const int* cp = &x; // pointer to const int — can't change *cp int* const pc = &x; // const pointer to int — can't change pc const int* const cpc = &x; // const pointer to const int — neither // ── void pointer ───────────────────────────────────────────── void* vp = &x; // can hold any address int* ip = static_cast<int*>(vp); // must cast to dereference
| const int* p | Pointer to const — the pointed-to value cannot be changed through p. The pointer itself can be reassigned. |
| int* const p | Const pointer — p cannot be reassigned to another address. The pointed-to value can be changed. |
| nullptr | The null pointer constant (C++11). Always prefer over NULL or 0. |
| void* | Generic pointer — holds any address. Must cast to the correct type before dereferencing. |
References & Rvalue References
03int x = 10; int& ref = x; // ref IS x — same object, different name ref = 99; // x is now 99 &ref == &x; // true — same address // ── References vs pointers ─────────────────────────────────── // References: must be initialized, can't be null, can't be reseated // Pointers: can be null, can be reassigned, can do arithmetic // ── Pass by reference ──────────────────────────────────────── void swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } swap(x, y); // x and y are swapped — no & needed at call site // ── Const reference — read-only alias, no copy ─────────────── void print(const std::string& s) { std::cout << s; } print("hello"); // no copy — string literal binds to const ref // ── Reference to temporary (lifetime extension) ─────────────── const int& r = 42; // ✅ const ref extends lifetime of temporary // int& r2 = 42; // ❌ non-const ref can't bind to temporary // ── Rvalue reference (C++11) — move semantics ───────────────── int&& rr = 42; // rvalue reference to temporary std::string&& sr = std::string("hello"); // sr owns the temporary
| int& ref = x | Lvalue reference — an alias. Must bind to an existing object. Cannot be null or reseated. |
| const T& r = tmp | Const ref to temporary extends the temporary's lifetime to match the reference's scope. |
| int&& rr | Rvalue reference — binds to temporaries. Foundation of move semantics. |
| std::move(x) | Casts x to an rvalue reference, enabling move construction/assignment. Does NOT move by itself. |
Smart Pointers
04#include <memory> // ── unique_ptr — sole ownership ────────────────────────────── std::unique_ptr<int> p = std::make_unique<int>(42); *p; // 42 p.get(); // raw int* (don't store — may dangle) p.reset(); // destroy the object p = nullptr; // same as reset() // Cannot copy — can only move auto q = std::move(p); // p is now null; q owns the int // unique_ptr for arrays auto arr = std::make_unique<int[]>(10); arr[0] = 5; // ── shared_ptr — shared ownership (reference counted) ──────── auto s1 = std::make_shared<int>(10); auto s2 = s1; // both point to the same int s1.use_count(); // 2 — two owners s2.reset(); // decrements count → 1 // Destroyed when count reaches 0 // ── weak_ptr — non-owning observer ─────────────────────────── std::weak_ptr<int> w = s1; // doesn't increment count if (auto locked = w.lock()) { // lock() returns shared_ptr or null *locked; // safe to use } // Breaks circular reference cycles between shared_ptrs
| make_unique<T>(...) | Preferred way to create a unique_ptr. Exception-safe. Never call new directly. |
| make_shared<T>(...) | Preferred way to create a shared_ptr. Single allocation for object + control block. |
| p.get() | Returns the raw pointer. Use only for C APIs. Don't store — the smart pointer still owns. |
| weak_ptr::lock() | Returns a shared_ptr if the object still exists, otherwise nullptr. Always check before use. |
Prefer unique_ptr by default. shared_ptr adds reference counting overhead on every copy. Only reach for shared_ptr when you genuinely need multiple owners.
Pointer vs Reference
05// Pointer (T*) Reference (T&) // Null? ✅ can be null ❌ must bind to object // Reseat? ✅ can point elsewhere ❌ always same object // Arithmetic? ✅ p++, p+n, p-q ❌ not supported // Syntax to access *p or p->m x or x.m (transparent) // Initialization can be uninitialized must be initialized // Use for optional, arrays, aliases, out-params, // dynamic allocation const params // ── When to use each ──────────────────────────────────────── // Pass non-optional in-param, read-only: const T& (or string_view) // Pass non-optional, modify caller's var: T& // Pass optional / nullable: T* // Return optional result: std::optional<T> // Own heap object, sole ownership: std::unique_ptr<T> // Own heap object, shared: std::shared_ptr<T> // Observe shared object without ownership: std::weak_ptr<T> // Raw pointer: C APIs, performance-critical internals
| const T& | Non-optional read-only parameter. Accepts lvalues and temporaries. Zero overhead. |
| T& | Non-optional out/in-out parameter. Caller must pass a named variable. |
| T* | Optional (nullable) parameter. Caller passes nullptr to opt out. |
| unique_ptr<T> | Sole ownership. Function takes ownership from the caller. |
Dangling Pointers & Common UB
06// ── Dangling pointer — points to freed memory ───────────────── int* p = new int(42); delete p; *p; // ❌ UB — use-after-free // Fix: null after delete delete p; p = nullptr; // Better fix: use unique_ptr — delete happens automatically // ── Dangling reference ──────────────────────────────────────── int& bad() { int x = 5; return x; // ❌ x is destroyed on return } // ── Stack overflow via pointer ──────────────────────────────── int arr[5]; int* p2 = &arr[5]; // past-the-end — valid to form *p2 = 0; // ❌ UB — out of bounds // ── Common tools to detect these ───────────────────────────── // -fsanitize=address — AddressSanitizer: catches use-after-free, // out-of-bounds, heap/stack overflows at runtime // -fsanitize=undefined — catches null dereference, misaligned access // Valgrind — memory leak and error detector
| use-after-free | Accessing memory after delete. Caught by AddressSanitizer (-fsanitize=address). |
| dangling ref | Returning a reference to a local variable. The local is destroyed; the caller gets garbage. |
| out-of-bounds | Accessing arr[n] or beyond. No exception — silent UB. Caught by ASan. |
| null deref | Dereferencing a nullptr. Caught by -fsanitize=undefined and ASan. |
Build with sanitizers during development.
-fsanitize=address,undefined catches the majority of pointer-related bugs at runtime with minimal setup.The this Pointer
07class Counter { int count = 0; public: // this — implicit pointer to the current object Counter& increment() { count++; return *this; // return ref to self — enables chaining } // Const member function — this is const Counter* int get() const { return count; // cannot modify count here // count++; // ❌ compile error in const function } // Distinguish member from parameter with same name void setCount(int count) { this->count = count; // this->count = member, count = param } }; Counter c; c.increment().increment().increment(); // chaining via *this c.get(); // 3
| this | Implicit pointer to the current object inside a member function. Type: T* (or const T* in const methods). |
| return *this | Returns the object by reference — enables method chaining (builder pattern). |
| const method | Declared with const after the parameter list. this is const T* — cannot modify data members. |
| this->member | Disambiguates when a local parameter shadows a data member of the same name. |