Phase 2 · Classes, Ownership & Resource SafetyModule 8~52 min read

Pointers, Lifetimes & Dynamic Memory

Understand addresses and lifetimes, distinguish ownership from observation, and replace fragile manual allocation with safer defaults.

What you'll learn

Pointers are not automatically dangerous; unclear lifetime and ownership are. You will learn to read an address, express optional observation, recognize invalid lifetimes, and usestd::unique_ptr when dynamic ownership is truly required.

By the end, you'll be able to:

  • Explain object lifetime separately from storage and scope
  • Use pointers for nullable observation and references for required aliases
  • Recognize leaks, dangling pointers, double deletion, and mismatched allocation
  • Replace manual dynamic memory with values, containers, and unique ownership

Objects and lifetimes

Every object has a lifetime: an interval during which its storage contains a valid object of its type. Automatic objects begin lifetime at their declaration and are destroyed when their scope exits. Container elements live while the container contains them. A dynamic object lives until its owner destroys it.

Scope controls where a name is visible; lifetime controls whether an object still exists. These often align for local variables, but a pointer can escape the name's scope and outlive the object it once observed.

Key idea

Before dereferencing an address, prove that it is non-null, still points to a live object, and permits the requested access.

Pointer fundamentals

A pointer stores an address. &object obtains an address and*pointer accesses the pointed-to object. nullptr represents no object and has a dedicated pointer-compatible type.

DeclarationMeaning
T* pNullable, reseatable pointer to T
const T* pPointer that cannot mutate T through p
T* const pFixed pointer that can mutate T
const T* const pFixed pointer with read-only access
pointers.cpp
#include <iostream>

void add_tax(double* price) {
    if (price != nullptr) {
        *price *= 1.1;
    }
}

int main() {
    double total{50.0};
    double* observer{&total};
    add_tax(observer);
    std::cout << *observer << '\n';
    add_tax(nullptr); // explicitly no object
}

References versus pointers

A reference expresses a required alias and cannot be reseated after initialization. A pointer naturally expresses “maybe an object,” can point elsewhere, and supports low-level traversal. Neither form owns an object unless an API explicitly says otherwise.

ContractPrefer
Required read-only inputconst T&
Required mutable inputT&
Optional observerconst T*
Exclusive dynamic ownerstd::unique_ptr<T>
Contiguous borrowed sequencestd::span<T>

Note

A raw pointer is often a perfectly good observer. The dangerous ambiguity is using the same raw pointer spelling to imply ownership and deletion responsibility.

Pointer arithmetic and arrays

Adding one to a pointer advances by one element, not one byte. Arithmetic is defined only within the same array object (plus its one-past-the-end position), and that end pointer may be compared but never dereferenced. Iterators and spans communicate the same traversal more safely in most application code.

pointer_range.cpp
#include <iostream>

int main() {
    int values[]{3, 6, 9, 12};
    int* current{values};
    int* end{values + 4};

    while (current != end) {
        std::cout << *current << ' ';
        ++current;
    }
}
This explains iterator mechanics; range-based for is clearer for this exact loop.

Manual dynamic memory

new creates a dynamic object and returns its address; deletedestroys it and releases storage. Arrays use the separate new[] anddelete[] forms. Missing, duplicating, or mismatching cleanup produces leaks or undefined behavior, especially across exceptions and early returns.

legacy_manual.cpp
// Mechanism demonstration, not the preferred design.
int* score{new int{95}};
// use *score while it is alive
delete score;
score = nullptr;

int* samples{new int[100]{}};
// use samples[0] ... samples[99]
delete[] samples;
samples = nullptr;
Production code should put the ownership in a standard type immediately.
  • A leak loses the last route to a live allocation
  • A dangling pointer retains an address after the object dies
  • Double deletion attempts to destroy the same dynamic object twice
  • Use-after-free accesses storage after its object lifetime ended

Watch out

Setting one pointer to nullptr does not repair any other aliases that point to the deleted object. Ownership structure prevents the bug more reliably than pointer cleanup rituals.

Exclusive ownership with unique_ptr

std::unique_ptr<T> owns one dynamic object and destroys it automatically. It cannot be copied, because two exclusive owners would contradict the contract, but it can be moved to transfer ownership. std::make_unique constructs the object and owner together.

unique_owner.cpp
#include <iostream>
#include <memory>
#include <string>
#include <utility>

struct Job {
    std::string name;
    ~Job() { std::cout << "finished " << name << '\n'; }
};

int main() {
    auto first{std::make_unique<Job>(Job{"compile"})};
    std::unique_ptr<Job> active{std::move(first)};

    std::cout << active->name << '\n';
    std::cout << std::boolalpha << (first == nullptr) << '\n';
}

Tip

Pass a unique_ptr by value when a function takes ownership. Pass T&, const T&, or T* when it only uses the object temporarily.

Safer allocation defaults

Most programs need fewer dynamic allocations than beginners expect. Return values directly, store variable-length data in std::vector or std::string, compose objects by value, and use std::optional for an optional value. Reach for a smart pointer when identity, polymorphism, lifetime transfer, or a stable address actually requires it.

  • Prefer an automatic value for one local object
  • Prefer a standard container for a dynamic collection
  • Prefer unique_ptr for exclusive dynamic ownership
  • Treat raw pointers and references as borrowed access
  • Document every API boundary that transfers ownership

Recap & quick check

Key takeaways

  • Lifetime determines whether an object exists; scope determines where a name is visible.
  • A reference is a required alias, while a raw pointer can naturally represent optional observation.
  • Pointer arithmetic is valid only within one array and its one-past endpoint.
  • Manual new/delete exposes leaks and lifetime bugs; unique_ptr provides automatic exclusive ownership.
  • Values, strings, and containers are better defaults than direct dynamic allocation.

Quick check

1. Which value represents a pointer that observes no object?

2. What does a raw pointer guarantee about ownership?

3. Why can unique_ptr not be copied?

4. Which is usually best for 100 runtime-created integers?

Next: Module 9 — Classes, Objects & Encapsulation, where ownership and validation become part of reusable domain types.