C++ • Memory and Pointers

Smart Pointers — Use smart pointers (unique_ptr, shared_ptr) at an introductory level

Automate memory management and eliminate leaks with RAII-based pointer wrappers.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 4 figures

SECTION 1

Historical Context & Motivation

From its earliest days, C++ inherited C's manual memory management model: every call to new had to be paired with a corresponding call to delete, and every new[] with delete[]. This pairing is deceptively simple in toy programs, but in production codebases with exceptions, early returns, and complex ownership graphs, developers routinely introduced memory leaks, dangling pointers, and double-free errors — entire categories of bugs that were notoriously difficult to reproduce and diagnose.

The C++ community recognized early on that the language's own object lifetime semantics — specifically the guarantee that destructors run when objects leave scope — could be leveraged to manage heap memory automatically. This principle is known as RAII (Resource Acquisition Is Initialization), and it became the philosophical cornerstone upon which smart pointers were built. The journey from flawed early attempts to the robust facilities in C++11 and beyond spans over two decades of language evolution.

1998 • auto_ptr in C++98

The C++98 standard introduced std::auto_ptr, the first standard-library smart pointer. It attempted to model unique ownership but had deeply problematic copy semantics — copying an auto_ptr silently transferred ownership, breaking containers and causing subtle bugs.

2001 • Boost Smart Pointers

The Boost C++ Libraries introduced boost::shared_ptr and boost::scoped_ptr, providing reference-counted shared ownership and non-copyable exclusive ownership respectively. These designs proved far more practical and became de facto industry standards.

2011 • C++11 — Modern Smart Pointers

C++11 deprecated auto_ptr and introduced std::unique_ptr (leveraging move semantics) and std::shared_ptr with std::weak_ptr. These became the recommended tools for heap memory management.

2014 • make_unique arrives in C++14

C++14 added std::make_unique, closing a gap from C++11 and giving developers a uniform, exception-safe factory function that parallels std::make_shared. The Core Guidelines formalized the advice: prefer smart pointers over raw new/delete.

The central question that smart pointers answer is this: how can we guarantee that dynamically allocated memory is freed exactly once, at the right time, without requiring the programmer to manually track every allocation path? Understanding smart pointers is fundamental to writing correct, modern C++ — and to understanding ownership semantics that appear across systems programming languages such as Rust.

SECTION 2

Core Principles & Definitions

Smart pointers are class templates in the <memory> header that wrap a raw pointer and manage the lifetime of the pointed-to object. They overload operator* and operator-> so they behave syntactically like raw pointers while providing automatic cleanup through their destructors. The design relies on several core principles that together form a coherent ownership model.

1

RAII — Resource Acquisition Is Initialization

Resources are acquired in constructors and released in destructors. Because C++ guarantees destructor execution when an object leaves scope (even via exceptions), wrapping a raw pointer in a stack-allocated object ensures deterministic deallocation.
2

Exclusive Ownership (unique_ptr)

Exactly one unique_ptr owns the resource at any time. It cannot be copied, only moved. When it is destroyed or reset, it deletes the managed object. This is the default choice for single-owner scenarios.
3

Shared Ownership (shared_ptr)

Multiple shared_ptr instances can co-own the same object. An internal reference count tracks how many owners exist; the object is deleted only when the last owner is destroyed or reset.
4

Move Semantics

Move semantics, introduced in C++11, allow transfer of ownership without copying. A std::move on a unique_ptr transfers the internal raw pointer and nullifies the source. This is the mechanism that makes unique_ptr practical.
5

Factory Functions (make_unique / make_shared)

These functions construct the managed object and the smart pointer in a single expression, providing exception safety and (for make_shared) a single allocation for both the object and the control block. Prefer these over raw new.
✦

KEY TAKEAWAY

Think of a smart pointer as a safety deposit box at a bank. A unique_ptr is a box with exactly one key — whoever holds the key can access the contents, but if they hand the key to someone else (move), they no longer have access, and when the last key-holder leaves, the box is emptied automatically. A shared_ptr is a box with multiple copies of the key; the bank tracks how many copies exist, and only empties the box when every copy has been returned.

SECTION 3

Visual Explanation — Ownership Models

The diagram below contrasts three memory management strategies: raw pointers with manual delete, unique_ptr exclusive ownership, and shared_ptr reference-counted ownership. Follow each column from top to bottom to see how the heap object's lifetime is managed in each case.

Ownership Models ComparedRaw Pointerunique_ptrshared_ptrint* p = new int;Heap: intp goes out of scope...⚠ MEMORY LEAKNo automatic delete called.Heap object persists forever.unique_ptr<int> pHeap: intp goes out of scope...~unique_ptr() calls delete✓ Memory freedshared_ptr<int> ashared_ptr<int> b = aHeap: intref_count = 2a goes out of scope...ref_count = 1b goes out of scope...ref_count = 0~shared_ptr() calls delete✓ Memory freed
Left column: a raw pointer leaks when its owning scope ends. Center column: unique_ptr automatically deletes the heap object in its destructor. Right column: shared_ptr decrements a reference count on each destruction and deletes the heap object when the count reaches zero.

Notice that in both the unique_ptr and shared_ptr columns, the programmer never writes delete. The destructor — triggered automatically by scope exit, stack unwinding, or container destruction — handles deallocation. This is the essence of RAII applied to dynamic memory: the object's lifetime on the heap is tied to the lifetime of the smart pointer on the stack.

SECTION 4

How Smart Pointers Work Internally

unique_ptr Internals

A std::unique_ptr<T> is a lightweight wrapper that stores a single raw pointer internally. It has a deleted copy constructor and copy assignment operator, which means the compiler will refuse any attempt to duplicate it. Ownership can only be transferred via std::move(), which invokes the move constructor: the source pointer is copied to the destination, then the source is set to nullptr. Because the wrapper adds no reference count and no control block, a unique_ptr typically has zero overhead compared to a raw pointer — sizeof(unique_ptr<T>) equals sizeof(T*) when using the default deleter.

ℹ

Key API Surface — unique_ptr

std::make_unique<T>(args...) — constructs a T on the heap and returns a unique_ptr<T>. Use .get() to obtain the raw pointer without transferring ownership. Use .release() to relinquish ownership (returns raw pointer, sets internal pointer to null). Use .reset(ptr) to replace the managed object (deletes the old one first).

shared_ptr Internals

A std::shared_ptr<T> stores two things: a pointer to the managed object and a pointer to a control block. The control block is a heap-allocated structure that contains a strong reference count (tracking shared_ptr copies) and a weak reference count (tracking weak_ptr instances). Copying a shared_ptr increments the strong count atomically; destroying one decrements it. When the strong count drops to zero, the managed object is deleted. The control block itself persists until both counts reach zero.

ℹ

Key API Surface — shared_ptr

std::make_shared<T>(args...) — allocates the object and control block in a single allocation, improving cache locality and exception safety. Use .use_count() to inspect the current strong reference count. Use .unique() (deprecated in C++20) to check if use_count() == 1.

Because shared_ptr maintains an atomic reference count, it incurs non-trivial overhead compared to unique_ptr: each copy and destruction requires an atomic increment or decrement, and the control block itself occupies additional memory (typically 16–32 bytes depending on the implementation). This is why the C++ Core Guidelines recommend defaulting to unique_ptr and escalating to shared_ptr only when shared ownership is genuinely required.

SECTION 5

Lifecycle & Reference Count Walkthrough

To solidify your understanding of shared_ptr's reference counting, the diagram below traces the strong reference count through a sequence of operations on a shared resource. Each operation either creates a new shared_ptr (incrementing the count) or destroys/resets one (decrementing the count). The managed object is deleted at the precise moment the count reaches zero.

shared_ptr Reference Count Over Time0123ref_countmake_shared()copy tobcopy tocc.reset()b out ofscopea out ofscopedelete calleda ownsa, b owna, b, c own
The amber step-line traces the strong reference count as shared_ptr instances are created (count rises) and destroyed or reset (count falls). The red dot at count = 0 marks the moment the managed object is automatically deleted.

A crucial detail is that the reference count operations are atomic — thread-safe by default. This means that multiple threads can safely copy and destroy shared_ptr instances pointing to the same object without data races on the count itself. However, this atomicity does not protect the managed object from concurrent access; you still need mutexes or other synchronization for that. The atomicity also explains part of shared_ptr's runtime cost: atomic operations are significantly more expensive than plain increments on most architectures.

SECTION 6

Worked Example — A Resource Manager

Consider a scenario where you are building a simple logging system. A Logger class wraps a file handle. You want a factory function to create loggers and pass them to processing components. The question is: should ownership be unique or shared? We will implement both approaches and analyze the trade-offs.

unique_ptr Logger — Exclusive Ownership

  1. 1

    Step 1 — Define the Logger class

    We define a simple Logger class with a constructor that opens a file and a destructor that closes it. The destructor prints a message so we can observe when cleanup occurs. class Logger { public: Logger(const std::string& file) : filename(file) { std::cout << "Logger opened: " << filename << '\n'; } ~Logger() { std::cout << "Logger closed: " << filename << '\n'; } void log(const std::string& msg) { std::cout << "[" << filename << "] " << msg << '\n'; } private: std::string filename; };
  2. 2

    Step 2 — Create with make_unique

    We use std::make_unique to create the Logger on the heap. The returned unique_ptr is the sole owner. auto logger = std::make_unique<Logger>("app.log"); logger->log("Application started.");
    Output: Logger opened: app.log followed by [app.log] Application started.
  3. 3

    Step 3 — Transfer ownership with std::move

    To pass the Logger to another function, we move it. After the move, the original logger variable is nullptr and must not be dereferenced. void process(std::unique_ptr<Logger> log) { log->log("Processing data..."); } // ~Logger() called here — log destroyed at scope exit process(std::move(logger)); // logger is now nullptr
    Output includes Logger closed: app.log when process() returns — no manual delete needed.
  4. 4

    Step 4 — Compile-time safety

    If we accidentally try to copy the unique_ptr instead of moving it, the compiler emits an error: "use of deleted function 'unique_ptr(const unique_ptr&)'". This compile-time enforcement is a key advantage over raw pointers, where accidental aliasing goes undetected.
    Ownership violations are caught at compile time, not at runtime.

shared_ptr Logger — Shared Ownership

  1. 1

    Step 1 — Create with make_shared

    When multiple subsystems need access to the same Logger, we use shared_ptr. auto logger = std::make_shared<Logger>("shared.log"); std::cout << "ref count: " << logger.use_count() << '\n'; // 1
  2. 2

    Step 2 — Copy to another shared_ptr

    Copying a shared_ptr increments the reference count. auto loggerCopy = logger; std::cout << "ref count: " << logger.use_count() << '\n'; // 2
    Both logger and loggerCopy point to the same Logger object. ref_count = 2.
  3. 3

    Step 3 — Observe automatic cleanup

    When the last shared_ptr owning the Logger is destroyed, the Logger's destructor runs automatically. loggerCopy.reset(); // ref_count drops to 1 logger.reset(); // ref_count drops to 0 → delete called
    Output: Logger closed: shared.log — printed exactly once, at the correct moment.

SECTION 7

unique_ptr vs. shared_ptr — Choosing the Right Tool

Characteristicunique_ptrshared_ptr
Ownership modelExclusive — exactly one owner at a timeShared — multiple owners via reference counting
Copyable?No (move only)Yes (increments ref count)
Memory overheadZero — same size as a raw pointer (with default deleter)Control block allocation (~16–32 bytes)
Runtime overheadNone — identical to raw pointerAtomic increment/decrement on copy/destroy
Thread safetyNot applicable (single owner)Ref count is atomic; managed object is not protected
Typical use caseFactory return values, tree/graph children, compositionCaches, observer patterns, DAG nodes, plugin systems
Circular reference risk?NoYes — use weak_ptr to break cycles

Feature comparison between unique_ptr and shared_ptr

🎯

DESIGN GUIDELINE

Follow the principle of least privilege for ownership. Start with unique_ptr (exclusive, zero-cost, compile-time enforced). Only escalate to shared_ptr when you have proven that multiple independent lifetimes must co-own the same resource. Think of it like version control access: give everyone read-only by default (unique ownership), and only grant write access (shared ownership) when collaboration genuinely requires it.
⚠

Beware Circular References

If two objects hold shared_ptr instances to each other, neither reference count can ever reach zero, creating a memory leak. Break cycles by making one direction use std::weak_ptr — a non-owning observer that does not increment the strong count but can be promoted to a shared_ptr via .lock() when access is needed.

SECTION 8

Connection to Advanced Memory Management

Smart pointers in C++ represent just one point on the spectrum of automatic memory management strategies. Understanding where they sit relative to other approaches gives you perspective on why modern systems languages make the design choices they do. The table below positions smart pointers among related techniques you will encounter in advanced coursework and professional practice.

TechniqueMechanismDeterministic?Overhead
Manual new/deleteProgrammer responsibilityYesNone (but bug-prone)
unique_ptrRAII, move semanticsYesZero
shared_ptrRAII, atomic reference countingYesModerate (control block, atomics)
Rust ownership systemCompile-time borrow checkerYesZero runtime; strict compile-time rules
Garbage collection (Java, Go)Tracing GC (mark-and-sweep, generational)No (GC pauses)High (runtime tracing, memory overhead)

Memory management strategies across languages

As you advance, you will encounter custom deleters (used to release non-memory resources like file descriptors or GPU buffers), std::enable_shared_from_this (which allows an object to safely create shared_ptr instances pointing to itself), and allocator-aware smart pointers for performance-critical scenarios such as game engines and high-frequency trading systems. The foundational model of unique_ptr and shared_ptr you have learned here provides the conceptual scaffolding for all of these extensions.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why std::unique_ptr deletes its copy constructor and copy assignment operator. What problem would arise if copying were allowed, and how does this relate to the concept of exclusive ownership?
Show answer
If copying a unique_ptr were allowed, two unique_ptr instances would hold the same raw pointer. When the first one is destroyed, it would call delete on the managed object. The second would then hold a dangling pointer, and its subsequent destruction would produce a double-free — undefined behavior. By deleting the copy operations, the compiler enforces at compile time that only one unique_ptr can own the resource, and ownership transfer must be explicit via std::move(). This was the fundamental flaw in auto_ptr, which silently transferred ownership on copy, leading to use-after-move bugs.

Question 2: BASIC CALCULATION

Given the following code, trace the value of use_count() at each labeled point: auto a = std::make_shared<int>(42); // (1) auto b = a; // (2) auto c = b; // (3) b.reset(); // (4) c = nullptr; // (5)
Show answer
At point (1), a is the sole owner, so use_count() = 1. At (2), copying to b increments the count to 2. At (3), copying to c increments to 3. At (4), b.reset() relinquishes b's share, dropping to 2. At (5), assigning nullptr to c relinquishes its share, leaving a as the sole owner with use_count() = 1. The managed int is not deleted until a itself goes out of scope.

Question 3: INTERMEDIATE

The following function has a bug. Identify the issue and fix it using appropriate smart pointers: Widget* createWidget(int id) { Widget* w = new Widget(id); if (!w->isValid()) { return nullptr; // BUG? } return w; }
Show answer
The bug is a memory leak: when isValid() returns false, the function returns nullptr without deleting w. The fix is to use unique_ptr: std::unique_ptr<Widget> createWidget(int id) { auto w = std::make_unique<Widget>(id); if (!w->isValid()) { return nullptr; // w's destructor deletes the Widget } return w; // ownership transferred to caller via move } Now the early return path automatically deletes the Widget because the unique_ptr destructor runs during stack unwinding. The caller receives ownership of the valid Widget through the returned unique_ptr.

Question 4: APPLIED

You are designing a scene graph for a 3D renderer. Each SceneNode owns its children but needs a back-pointer to its parent. Using shared_ptr for both the children and the parent pointer creates a cyclic reference. Explain the problem and propose a correct ownership design using the smart pointer types you have learned.
Show answer
If parent and child both hold shared_ptr to each other, a reference cycle forms: the parent's count never reaches zero because the child holds a reference to it, and vice versa. Neither object is ever deleted. The standard solution is to use shared_ptr for the owning direction (parent → children via std::vector<std::shared_ptr<SceneNode>>) and std::weak_ptr<SceneNode> for the back-pointer (child → parent). The weak_ptr does not contribute to the reference count, so when the parent is no longer referenced externally, its count can reach zero and it is deleted. Alternatively, if the parent's lifetime strictly contains the children's lifetimes, using unique_ptr for children and a raw (non-owning) pointer for the parent back-reference is even simpler and more efficient.

Question 5: CRITICAL THINKING

The C++ Core Guidelines state: "R.20: Use unique_ptr or shared_ptr to represent ownership" and "R.21: Prefer unique_ptr over shared_ptr unless you need to share ownership." Critically evaluate these guidelines. Under what circumstances might you legitimately choose raw pointers or a completely different strategy? Consider performance-critical systems, embedded environments, and interoperability with C APIs.
Show answer
While smart pointers are the default best practice, several legitimate exceptions exist. In embedded systems with no dynamic allocation (bare-metal, MISRA-compliant codebases), objects are placed in static storage or on the stack, and smart pointers are irrelevant. When interfacing with C APIs that expect raw pointers and manage their own lifetimes (e.g., FILE* from fopen), wrapping in a unique_ptr with a custom deleter is possible but sometimes adds complexity without benefit, especially for short-lived resources. In performance-critical hot paths (game engine inner loops, HFT), even shared_ptr's atomic operations may be unacceptable; here, arena allocators or object pools with raw pointers and manual lifetime management outperform. Non-owning raw pointers remain appropriate and recommended when a function merely observes an object whose lifetime is managed elsewhere — passing T* or T& clearly signals "I don't own this." The guidelines are designed for general-purpose application code, and applying them thoughtfully — rather than dogmatically — is a hallmark of engineering maturity.

SUMMARY

Summary

Summary

Smart pointers bring RAII-based automatic memory management to C++ heap allocations, eliminating entire categories of bugs including memory leaks, dangling pointers, and double frees. std::unique_ptr provides exclusive ownership with zero runtime overhead, enforcing single-owner semantics at compile time through deleted copy operations and move semantics. std::shared_ptr enables shared ownership via an atomic reference count stored in a control block; the managed object is deleted when the last owner is destroyed.

The design guideline is to prefer unique_ptr by default, escalating to shared_ptr only when genuinely shared lifetimes exist. Always use the factory functions make_unique and make_shared for exception safety and clarity. When shared_ptr creates potential reference cycles, break them with std::weak_ptr. Together, these tools form the foundation of modern C++ resource management and prepare you for advanced topics including custom deleters, allocator-aware containers, and cross-language ownership models like Rust's borrow checker.

Varsity Tutors • C++ • Smart Pointers — Use smart pointers (unique_ptr, shared_ptr) at an introductory level