20 topics
/
← Back to Quick Reference
Topic 13

Pointers & References

Raw Pointers · References · Smart Pointers · Ownership · this · UB

C++17 · Advanced Reference

Pointers & References

01

Indirection 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. 1.Stack — fast, automatic lifetime. Local variables. Limited size (~1–8 MB).
  2. 2.Heap — dynamic, manual (or RAII) lifetime. new / delete or smart pointers.
  3. 3.A pointer is a variable that holds an address — 8 bytes on 64-bit systems.
  4. 4.A reference is an alias — same address as the original, no extra storage.

Ownership hierarchy

  1. 1.unique_ptr — sole owner. Zero overhead. Destroyed when it goes out of scope.
  2. 2.shared_ptr — shared ownership via reference count. Small overhead per dereference.
  3. 3.weak_ptr — non-owning observer. Must lock before use. Breaks cycles.
  4. 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

02
int 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* pPointer to const — the pointed-to value cannot be changed through p. The pointer itself can be reassigned.
int* const pConst pointer — p cannot be reassigned to another address. The pointed-to value can be changed.
nullptrThe 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

03
int 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 = xLvalue reference — an alias. Must bind to an existing object. Cannot be null or reseated.
const T& r = tmpConst ref to temporary extends the temporary's lifetime to match the reference's scope.
int&& rrRvalue 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-freeAccessing memory after delete. Caught by AddressSanitizer (-fsanitize=address).
dangling refReturning a reference to a local variable. The local is destroyed; the caller gets garbage.
out-of-boundsAccessing arr[n] or beyond. No exception — silent UB. Caught by ASan.
null derefDereferencing 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

07
class 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
thisImplicit pointer to the current object inside a member function. Type: T* (or const T* in const methods).
return *thisReturns the object by reference — enables method chaining (builder pattern).
const methodDeclared with const after the parameter list. this is const T* — cannot modify data members.
this->memberDisambiguates when a local parameter shadows a data member of the same name.