AlgoMaster Logo

Smart Pointers Overview

High Priority33 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A smart pointer is a class that wraps a raw pointer and adds automatic lifetime management: when the smart pointer goes out of scope, its destructor frees the memory the pointer was managing. That single change closes the door on most of the bugs that raw new and delete are famous for, including memory leaks, double-free, and dangling pointers from forgotten cleanup. This chapter introduces the idea, the standard library types that implement it (std::unique_ptr, std::shared_ptr, std::weak_ptr), and the trade-offs against raw pointers.

The Problem with Raw new and delete

Raw pointer ownership is a manual contract. Every new has to be paired with exactly one delete, on every code path, including exception paths. Get it wrong and the program either leaks memory or frees the same block twice. The shape of the bug is the same in each case:

The function allocates a Product on the heap. If the discount code is invalid, it returns early without cleaning up. The pointer p goes out of scope, but the memory it pointed at is still allocated, with no remaining reference to free it. That memory is leaked for the rest of the program's run.

Three classes of bug appear in raw-pointer code:

Memory leak. A new without a matching delete. The program holds onto memory it can no longer reach. Over time, leaks add up to processes that grow without bound.

Double-free. The same address gets passed to delete twice. The second free corrupts the heap's internal bookkeeping. The program might crash immediately or, worse, run for a while and then crash somewhere unrelated.

Dangling pointer (use-after-free). A pointer holds an address that has already been freed. Reading or writing through it accesses memory that may have been reused for something else entirely.

Three diagrams help see what is going on. First, the leak:

When process returns early, p is gone but the memory it pointed at is still allocated. The address is lost, so nothing can free it.

Second, the double-free:

Two pointers, one block. The first delete frees the block. The second delete corrupts the heap's free list, often causing a crash on the next allocation.

Third, the dangling pointer:

The pointer is still in scope, but its target is gone. Dereferencing it produces whatever the heap allocator has done with the memory since.

All three bugs share one root: the human has to remember to free at the right time, on every path. Smart pointers move that responsibility from the human to the type system.

Quick Check: Which of these patterns most often produces a memory leak in raw-pointer code?

  • A) Calling delete twice on the same pointer
  • B) An early return between new and the matching delete
  • C) Calling delete on a nullptr
  • D) Dereferencing a freed pointer

<details> <summary>Answer</summary>

B. An early return (or a thrown exception) between new and delete skips the cleanup. The pointer goes out of scope, the address is lost, and the memory cannot be reclaimed. A is double-free, C is harmless (delete nullptr is defined to do nothing), and D is use-after-free.

</details>

RAII: The Idea Smart Pointers Implement

The C++ pattern that fixes this is called RAII, short for "Resource Acquisition Is Initialization". The idea is older than the name: tie the lifetime of a resource (memory, file handle, socket, lock) to the lifetime of an object on the stack. When the stack object is destroyed, its destructor releases the resource. The compiler runs destructors automatically when scopes end, even on exceptions, so the cleanup happens regardless of how the function exits.

A small hand-written RAII wrapper for an int on the heap:

Output:

The destructor runs when a goes out of scope. The delete is no longer something the programmer has to remember. Even if a later line threw an exception, the destructor would still run as the stack unwound, and the memory would still be freed.

This wrapper is missing a few pieces (it cannot be safely copied, for one), but it shows the shape: a class owns a resource, and its destructor releases it. Smart pointers are this wrapper, written correctly and generalized to any type.

The smart pointer is a stack object. It owns a heap allocation. When it goes out of scope, the destructor frees the heap memory. The whole thing happens without explicit cleanup at the call site.

The Three Standard Smart Pointers

The C++ standard library provides three smart pointer class templates, all in the <memory> header. Each represents a different ownership model.

The header has been part of the standard library since C++98 (for auto_ptr, now removed) and modern usage starts with C++11, which added unique_ptr, shared_ptr, and weak_ptr. <memory> is a default include in modern C++ projects.

Smart pointerOwnership modelCost vs raw pointerWhen to use
std::unique_ptr<T>Exclusive: exactly one owner at a timeZero size overheadThe default. Use when one piece of code owns the object.
std::shared_ptr<T>Shared: many owners, freed when the last one disappearsReference count (heap-allocated control block)When ownership is genuinely shared across multiple parts of the program.
std::weak_ptr<T>Non-owning observer of a shared_ptr-owned objectSame control block as shared_ptrTo observe a shared object without keeping it alive. Breaks cycles.

A short example of each, to show the shape.

std::unique_ptr (Exclusive Ownership)

Output:

A unique_ptr cannot be copied. There is exactly one owner of the Product. The unique_ptr can be moved into another unique_ptr (transferring ownership), but never duplicated. This rules out double-free and most use-after-free bugs in code that respects the ownership boundary.

std::make_unique<Product>(...) is the recommended way to create a unique_ptr. It allocates and constructs the object in one expression, which is safer than unique_ptr<Product>(new Product(...)).

std::shared_ptr (Shared Ownership)

Output:

A shared_ptr keeps a reference count: how many shared_ptr objects refer to the same heap allocation. Copying a shared_ptr increments the count; destroying one decrements it. When the count reaches zero, the destructor frees the object.

The reference count lives in a separately-allocated control block alongside the object, so a shared_ptr is roughly the size of two raw pointers, and creating one has the cost of an extra allocation (unless make_shared is used, which combines the two allocations into one).

std::weak_ptr (Non-Owning Observer)

Output:

A weak_ptr holds a non-owning reference to an object managed by shared_ptr. It does not contribute to the reference count, so the object can be freed even while a weak_ptr still exists. To use the pointed-to object, the program calls lock(), which returns a shared_ptr that is either valid (if the object still exists) or empty (if it has been destroyed).

weak_ptr is the standard solution to cycles in shared ownership graphs, where two shared_ptr objects pointing at each other would never reach zero references.

The three types form a layered model. unique_ptr is the default. shared_ptr fits when ownership is genuinely shared. weak_ptr observes without owning, and it only makes sense in combination with shared_ptr.

Quick Check: A factory function builds a Product and gives it to one caller, which keeps it for the rest of the program. Which smart pointer type fits?

  • A) std::unique_ptr<Product>
  • B) std::shared_ptr<Product>
  • C) std::weak_ptr<Product>
  • D) A raw Product*

<details> <summary>Answer</summary>

A. There is exactly one owner, so unique_ptr fits. shared_ptr adds reference counting that is not needed. weak_ptr does not own anything and cannot be the result of a factory. A raw pointer puts the deletion burden on the caller, which is the problem smart pointers solve.

</details>

The <memory> Header

All three smart pointer types live in <memory>, along with the make_unique and make_shared factory functions and a few other utilities.

make_unique was added in C++14, slightly later than the C++11 introduction of unique_ptr itself. Older C++11-only code constructs unique_ptr directly with new:

The make_unique form is preferred for two reasons: it is exception-safe in argument lists (the raw new form has a known exception-safety hole when used as a function argument), and it avoids repeating the type name. For new code, use make_unique and make_shared rather than the raw-new constructor.

The <memory> header also defines several supporting utilities (allocators, std::allocator_traits, std::pointer_traits, std::uninitialized_* algorithms). For everyday code, the three smart pointers and the two make_* factory functions are the entire vocabulary.

Smart Pointer vs Raw Pointer: Trade-Offs

Smart pointers do not fit every situation. Raw pointers still have a place; the question is which job each one fits.

QuestionRaw pointerSmart pointer
Who owns the object?The programmer decides, often implicitThe type encodes it
How is the memory freed?Explicit delete somewhereAutomatic, on scope exit
Can it be null?YesYes
Can it leak?Yes, on forgotten cleanupNo (almost; cycles in shared_ptr can)
Can it dangle?Yes, easilyOnly if used incorrectly (e.g. get() then misuse)
Size overheadOne pointer (8 bytes on 64-bit)unique_ptr: same. shared_ptr: two pointers + control block
Runtime cost on copyOne pointer copyunique_ptr: cannot copy. shared_ptr: atomic increment
Use as function parameterCommonCommon, with conventions (see below)
Use for non-owning referenceCommonUse raw pointer or reference instead

The key distinction is ownership versus access. A smart pointer fits when the type needs to encode "this object is mine, and when I'm gone, the memory is freed". A raw pointer (or a reference) fits when the type just needs to point at something that someone else owns.

A common convention in modern C++ codebases:

  • A std::unique_ptr<T> parameter says "I take ownership of this object".
  • A std::shared_ptr<T> parameter says "I become one of the owners of this object".
  • A T* or T& parameter says "I do not own this; I just need to look at it for the duration of this call".

That last point is worth unpacking. A function that examines an object for the duration of its call does not need to participate in ownership. Wrapping a raw pointer in shared_ptr just to pass it as a parameter would force the caller to wrap its raw pointer, adding cost and obscuring the contract.

The shared-pointer parameter version still works, but it does extra work (atomic increment and decrement) for no reason. The reference version is cheaper and more truthful about the contract.

When Smart Pointers Are Not the Answer

Smart pointers fit heap-allocated objects whose lifetime needs explicit management. They do not fit every situation.

  • Stack-allocated objects. A local Product item{"Mouse", 24.99}; does not need a smart pointer at all. The compiler handles the destruction when the scope ends.
  • Values in containers. A std::vector<Product> already manages the lifetime of its elements. Wrapping each element in a unique_ptr adds an allocation per element with no benefit, unless the elements are polymorphic and need to be stored as base pointers.
  • Non-owning observers. If a function just reads from an object, a const T& or const T* parameter fits. Passing a shared_ptr by value bumps the reference count for no reason.
  • Performance-critical hot paths. shared_ptr reference counting uses atomic operations, which are not free. In tight loops or low-latency code, a raw pointer (or a reference, or an index) may be measurably faster. unique_ptr has no such overhead and remains a good choice.
  • C interop. A C API that takes a T* cannot take a unique_ptr<T> directly. Call .get() on the smart pointer to extract the raw pointer for the duration of the call, but do not let the C code take ownership.

The general rule: own with smart pointers, pass and look at with raw pointers or references. Mixing them this way avoids the worst bugs of raw-pointer ownership and the worst overhead of smart-pointer parameters.

Quick Check: Three of these are situations where a smart pointer fits. Which one does not?

  • A) A factory function that returns a newly-created object to the caller.
  • B) A member field that owns a polymorphic object stored as a base-class pointer.
  • C) A function parameter that just reads a single product to print its name.
  • D) A container of polymorphic objects where each element owns its own state.

<details> <summary>Answer</summary>

C. A function that only reads the object should take a const T& or const T* parameter. Wrapping the argument in a smart pointer for the call adds cost (an atomic increment for shared_ptr) or is impossible (unique_ptr cannot be copied). The other three are textbook smart-pointer use cases.

</details>

A Side-by-Side Rewrite

The leaky example from the beginning of this chapter, rewritten with std::unique_ptr:

Output:

Three changes from the raw-pointer version:

  • new Product{...} becomes std::make_unique<Product>(...).
  • The Product* becomes a std::unique_ptr<Product>.
  • The delete p; line is removed.

The early return no longer leaks. When p goes out of scope, its destructor runs, and the Product is freed. The same cleanup happens on the normal return path. Both paths are correct without any delete written by the programmer.

The -> and the null-checking idioms work the same way on a unique_ptr as on a raw pointer:

unique_ptr provides operator bool and operator->, so the calling code reads almost identically to raw-pointer code. The only difference is that the lifetime is now the smart pointer's job, not the programmer's.

A Worked Example: A Polymorphic Cart

A more realistic example. A shopping cart holds items of different types, each with its own pricing rule. With raw pointers, the cart has to remember to delete each item. With unique_ptr, the cart manages lifetimes automatically.

Output:

A few details about this example:

The cart is std::vector<std::unique_ptr<Item>>. The vector owns the smart pointers, and each smart pointer owns one polymorphic item. When the vector is destroyed (when main returns, in this case), it destroys each smart pointer in turn, which destroys each item. No manual cleanup, no leak, no risk of double-free.

The items are polymorphic, so they are stored by base-class pointer. This is the canonical use case for unique_ptr in containers: storing different concrete types behind a common interface. Plain std::vector<Item> would not work, because Item is abstract and cannot be sliced into the vector.

Calls to item->price() and item->label() use -> the same way raw pointers do. The dispatch is virtual, so each call lands in the correct derived class. The smart pointer's wrapping is invisible at the call site.

Interview Questions

Q1: What problems do smart pointers solve that raw pointers do not?

Smart pointers tie heap-memory lifetime to a stack object's lifetime. The stack object's destructor runs automatically when its scope ends (including during exception unwinding), so the memory is freed on every code path. This eliminates the most common raw-pointer bugs: memory leaks from forgotten delete, double-free from confusion about ownership, and use-after-free when one piece of code frees while another still holds the pointer. Smart pointers also document ownership in the type system, so a function signature can declare "I take ownership" or "I share ownership" instead of leaving it as a comment.

Q2: When would you use `std::unique_ptr` over `std::shared_ptr`?

Use unique_ptr whenever exactly one piece of code owns the object. It is cheaper (no reference count, no atomic operations, no separately-allocated control block) and clearer about ownership. The default choice for new code is unique_ptr. Use shared_ptr only when the object genuinely needs multiple owners with independent lifetimes, such as a node shared across a graph or a resource cached by multiple subsystems. Many codebases find that they overuse shared_ptr; refactoring to unique_ptr usually clarifies the design.

Q3: What is the purpose of `std::weak_ptr`?

A weak_ptr observes an object owned by shared_ptr without contributing to the reference count. Its main use is breaking cycles: if two shared_ptr objects point at each other, the reference count never reaches zero and the objects leak. Making one direction a weak_ptr lets the cycle decay correctly. A weak_ptr is also useful for caches and listener lists, where the observer should not keep the observed object alive but needs a safe way to check whether it still exists. To use the object, call lock(), which returns a shared_ptr that may be empty if the object has been destroyed.

Q4: Why is `std::make_unique` preferred over `std::unique_ptr<T>(new T(...))`?

Two reasons. First, make_unique is exception-safe in function call argument lists. With f(std::unique_ptr<T>(new T()), g()), the compiler is allowed to interleave the new, the constructor of unique_ptr, and the call to g() in any order. If g() throws between new T() and the unique_ptr constructor, the allocated memory leaks. make_unique packages the allocation and the wrapping into one expression, closing that gap. Second, make_unique<T>(...) avoids repeating the type name compared to unique_ptr<T>(new T(...)), which keeps long declarations readable. The same logic applies to make_shared versus shared_ptr(new T(...)).

**Q5: A function only needs to read from a unique_ptr<Product> to print its name. Should the parameter be std::unique_ptr<Product>, const std::unique_ptr<Product>&, const Product*, or const Product&?**

const Product& fits best. The function does not need ownership, and a reference is the cheapest, clearest way to express "look at this object". Taking a unique_ptr by value would transfer ownership, which is wrong for a read-only operation. Taking it by reference works but ties the caller to using a unique_ptr (callers with raw pointers or stack objects could not pass their data). const Product* is fine if the parameter may be null. The general rule: function parameters express access, not ownership, unless the function genuinely takes ownership.

Exercises

Exercise 1: Convert this leaky function to use std::unique_ptr.

Expected Output:

<details> <summary>Solution</summary>

The early return no longer leaks. The unique_ptr destructor frees the int automatically.

</details>

Exercise 2: What is the output, and why?

Expected Output:

<details> <summary>Solution</summary>

The unique_ptr a is declared inside the inner block. When the block ends, a goes out of scope, the destructor runs, and Product's destructor prints Destroyed Mouse. Only after that does the next line print Outside scope.

</details>

Exercise 3: Write a factory function createProduct(const std::string& name, double price) that returns a std::unique_ptr<Product>. Use it in main.

Expected Output:

<details> <summary>Solution</summary>

The factory returns ownership to the caller. The caller's unique_ptr destructor frees the object at end of scope.

</details>

Exercise 4: Predict the output.

Expected Output:

<details> <summary>Solution</summary>

Each shared_ptr copy increments the reference count. a starts at 1. b = a makes it 2. c = a inside the block makes it 3. When the block ends, c is destroyed and the count drops back to 2. The int is not freed until the count reaches 0, which would happen after a and b both go out of scope at the end of main.

</details>

Exercise 5: Why does this code crash, and how would you fix it?

<details> <summary>Solution</summary>

Both a and b are constructed from the same raw pointer raw. When b is destroyed at the end of main, it deletes the Product. Then a is destroyed and tries to delete the same Product again. This is a double-free, which corrupts the heap and almost always crashes.

The fix is to never construct two unique_ptr objects from the same raw pointer. If shared ownership is needed, use shared_ptr. If sole ownership is needed, only one unique_ptr should ever own the object:

</details>

Exercise 6: Fill in the type. A function cheapest takes a vector of products and returns the address of the cheapest one. The vector is owned by the caller and stays alive for the duration of the call. What type should the return be?

<details> <summary>Solution</summary>

The function does not own the result; it just identifies an element inside the caller's vector. A raw const Product* (or a reference, with a sentinel "not found" return for empty input) fits the situation. A smart pointer would be wrong here, because the function does not allocate anything. A null pointer can also signal "the vector was empty" if needed.

</details>

Exercise 7: Convert this code from raw pointers to smart pointers. Use the most appropriate smart pointer type.

<details> <summary>Solution</summary>

There is exactly one owner, so unique_ptr is the correct choice. The explicit delete is gone; the smart pointer's destructor handles it.

</details>

Exercise 8: Two shared_ptr objects manage the same Product. The first owner reads the price, then goes out of scope. The second owner reads the price after. Will the Product be valid for the second read?

<details> <summary>Solution</summary>

Yes. shared_ptr keeps a reference count. When the first owner goes out of scope, the count drops from 2 to 1, but the object is not freed because there is still another owner. The second owner can still safely access the Product. The object is only freed when the last shared_ptr referring to it is destroyed.

</details>

Exercise 9: Why is this code suspect?

<details> <summary>Solution</summary>

The function only reads item->name. It does not need ownership or shared ownership. Taking shared_ptr<Product> by value forces every caller to use a shared_ptr, and the call itself pays the cost of an atomic increment to bump the reference count. A cleaner signature is void process(const Product& item) (or const Product* if null is meaningful), which accepts the object by reference without participating in ownership.

</details>

Exercise 10: Name one situation where a weak_ptr fits.

<details> <summary>Solution</summary>

A cache that holds references to objects without keeping them alive: the cache maps keys to weak_ptr<Resource>. As long as some other part of the program holds a shared_ptr to the resource, the cache can return it via lock(). Once no one else holds the resource, the cache's weak_ptr becomes empty and lock() returns nullptr, so the cache knows to evict the entry.

Another common use is breaking cycles: a parent node holds shared_ptr to its children, but each child holds a weak_ptr back to its parent. Without the weak_ptr, the parent-child cycle would never drop to zero references and the tree would leak.

</details>

Quiz

Smart Pointers Overview Quiz

10 quizzes