C++ Smart Pointers and Dynamic Memory

Understand stack and heap memory, why raw new and delete are risky, and how unique_ptr and shared_ptr manage memory for you.

Lesson 21 of 44Advanced18 min read

Most variables you've created so far live on the stack: they're created when a function or block starts and destroyed automatically when it ends. That's fast and safe, but sometimes an object needs to outlive the block that created it, or you don't know until the program runs how many objects you'll need. Those objects live on the heap, also called dynamic memory.

In older C++, heap memory was managed by hand with new and delete. Forgetting a delete leaks memory, deleting twice crashes the program, and using memory after it's been deleted causes some of the hardest bugs in software. Modern C++ solves this with smart pointers: objects that own heap memory and release it automatically.

In this lesson you'll see how stack and heap differ, why raw new and delete are risky, and how std::unique_ptr and std::shared_ptr make ownership clear and memory leaks almost impossible. It builds on the pointers lesson.

Stack, heap, new and delete

C++
void demo() {
    int a = 5;                 // stack: freed automatically
    int* b = new int(7);       // heap: lives until deleted
    delete b;                  // must free it yourself
}
  • new creates an object on the heap and returns a pointer to it.
  • delete destroys it and frees the memory. Arrays use new[] and delete[].
  • Forget the delete and the memory is leaked until the program ends.
  • Use the pointer after delete and you have a dangling pointer: undefined behaviour.

Getting every delete right is hard, especially when functions can return early or throw exceptions. That's why modern C++ code almost never writes new and delete directly.

RAII and std::unique_ptr

RAII (Resource Acquisition Is Initialisation) means tying a resource to an object's lifetime: the constructor acquires it and the destructor releases it. You've already used RAII with vector, string and ofstream.

std::unique_ptr applies RAII to heap objects. It's the only owner of the object it points to, and deletes it automatically when the unique_ptr itself is destroyed:

C++
#include <memory>

auto player = make_unique<Player>("Ana");   // create on the heap
player->score = 10;                         // use it like a pointer
// no delete needed

make_unique<T>(args...) creates the object and the unique_ptr in one step. Use it instead of new.

Moving ownership

A unique_ptr can't be copied, because two owners would both try to delete the same object. Ownership can be moved instead with std::move:

C++
auto a = make_unique<int>(5);
auto b = move(a);    // b owns the int now; a is empty (nullptr)

That's how you transfer an object into a function, return it from a function or put it into a container: vector<unique_ptr<Shape>> holds objects of different derived classes and deletes them all when the vector is destroyed, exactly as in the inheritance lesson.

shared_ptr and weak_ptr

Sometimes several parts of a program really do share one object, and it should only be deleted when the last of them is finished with it. std::shared_ptr keeps a reference count: each copy adds one, each destroyed copy subtracts one, and the object is deleted when the count reaches zero.

C++
auto config = make_shared<Settings>();
auto copy = config;               // both own it; use_count() is 2

Shared ownership is convenient but has a cost, and two objects that hold shared_ptrs to each other never reach zero, leaking both. std::weak_ptr breaks such cycles: it observes a shared object without owning it. As a rule, reach for unique_ptr first and use shared_ptr only when ownership is genuinely shared.

C++ Smart Pointers Syntax

Smart pointer syntax

#include <memory>

auto u = make_unique<Type>(args);
u->member;   *u;   u.get();
auto u2 = move(u);              // transfer ownership

auto s = make_shared<Type>(args);
auto s2 = s;                    // shared ownership
s.use_count();

vector<unique_ptr<Base>> items;
items.push_back(make_unique<Derived>(args));

C++ Smart Pointers Summary

  • Stack variables are freed automatically; heap objects live until freed.
  • Raw new and delete risk leaks, double deletes and dangling pointers.
  • RAII ties resources to object lifetimes; smart pointers apply it to heap memory.
  • unique_ptr is a single owner, created with make_unique, moved with std::move.
  • shared_ptr counts owners and deletes at zero; weak_ptr observes without owning.

C++ Smart Pointers Examples

Each example shows the code, the output it produces and what each part does.

Example 1

unique_ptr cleans up automatically

C++
#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Player {
    string name;
public:
    Player(const string& n) : name(n) { cout << name << " joined" << endl; }
    ~Player() { cout << name << " left" << endl; }
    void play() const { cout << name << " is playing" << endl; }
};

int main() {
    auto ana = make_unique<Player>("Ana");
    {
        auto ben = make_unique<Player>("Ben");
        ben->play();
    }
    ana->play();
    cout << "End of main" << endl;
    return 0;
}
Output
Ana joined
Ben joined
Ben is playing
Ben left
Ana is playing
End of main
Ana left
Line-by-line explanation
  1. Line 15make_unique creates Ana on the heap and returns the owning pointer.
  2. Lines 17 to 18Ben's unique_ptr lives only inside this block.
  3. Line 19At the closing brace, the unique_ptr is destroyed and deletes Ben automatically.
  4. Line 23Ana is deleted when main ends. No delete was written anywhere.
Example 2

Moving ownership into a container

C++
#include <iostream>
#include <memory>
#include <vector>
using namespace std;

struct Task {
    string title;
    Task(const string& t) : title(t) {}
};

int main() {
    vector<unique_ptr<Task>> tasks;

    auto first = make_unique<Task>("Write report");
    tasks.push_back(move(first));
    tasks.push_back(make_unique<Task>("Email team"));

    cout << "first is now " << (first ? "full" : "empty") << endl;
    for (const auto& t : tasks) {
        cout << "- " << t->title << endl;
    }
    return 0;
}
Output
first is now empty
- Write report
- Email team
Line-by-line explanation
  1. Line 15push_back(first) wouldn't compile, because unique_ptr can't be copied; move transfers ownership.
  2. Line 16A temporary from make_unique can be moved in directly.
  3. Line 18After the move, first holds nullptr.
  4. Lines 19 to 21The vector owns both tasks and deletes them when it's destroyed.
Example 3

shared_ptr and the reference count

C++
#include <iostream>
#include <memory>
#include <string>
using namespace std;

struct Settings {
    string theme = "dark";
    ~Settings() { cout << "Settings deleted" << endl; }
};

int main() {
    auto s1 = make_shared<Settings>();
    cout << "Owners: " << s1.use_count() << endl;
    {
        auto s2 = s1;
        s2->theme = "light";
        cout << "Owners: " << s1.use_count() << ", theme " << s1->theme << endl;
    }
    cout << "Owners: " << s1.use_count() << endl;
    s1.reset();
    cout << "After reset" << endl;
    return 0;
}
Output
Owners: 1
Owners: 2, theme light
Owners: 1
Settings deleted
After reset
Line-by-line explanation
  1. Line 15Copying a shared_ptr adds an owner; both point to the same Settings.
  2. Lines 16 to 17A change through s2 is visible through s1.
  3. Line 18s2 is destroyed, so the count drops back to 1.
  4. Line 20reset releases the last owner, so the object is deleted immediately.

Common C++ Smart Pointers Mistakes

Forgetting delete

Don't
void load() {
    Player* p = new Player("Ana");
    if (error) return;        // leak: p is never deleted
    delete p;
}
Do
void load() {
    auto p = make_unique<Player>("Ana");
    if (error) return;        // p is deleted automatically
}

Copying a unique_ptr

Don't
auto a = make_unique<int>(5);
auto b = a;               // error: copy constructor is deleted
Do
auto a = make_unique<int>(5);
auto b = move(a);         // a is now empty

Using shared_ptr everywhere

Don't
shared_ptr<Player> p = make_shared<Player>("Ana");   // only one owner ever
Do
unique_ptr<Player> p = make_unique<Player>("Ana");

shared_ptr adds counting overhead and hides who really owns the object. Use it only for genuinely shared ownership.

C++ Smart Pointers in Real Projects

Smart pointers in modern C++

Modern codebases at companies such as Google and Microsoft discourage raw new and delete in their style guides. Game engines own their entities through unique_ptr, GUI frameworks share resources like textures with shared_ptr, and tools such as AddressSanitizer exist mainly to catch the memory bugs that smart pointers prevent.

C++ Smart Pointers FAQ

What do reset and release do on a unique_ptr?

p.reset() deletes the object the pointer owns and leaves p empty; p.reset(new Thing) swaps in a new object after deleting the old one. p.release() gives up ownership without deleting: it returns the raw pointer and leaves p empty, so you're responsible for deleting the object yourself. You'll rarely need release outside code that talks to older APIs.

Can a unique_ptr manage an array?

Yes: auto nums = make_unique<int[]>(100); creates 100 ints, and nums[0] indexes them. The [] in the type makes it call delete[] correctly when it's destroyed. A vector<int> is usually a better choice, though, because it knows its size and can grow.

Are smart pointers slower than raw pointers?

A unique_ptr is normally the same size as a raw pointer and costs essentially nothing extra; it simply calls delete for you at the right time. A shared_ptr is bigger, typically two pointers, and every copy updates a reference count with a thread-safe atomic operation. That overhead is small, but it's another reason to prefer unique_ptr unless ownership is truly shared.

Can I convert a unique_ptr into a shared_ptr?

Yes, by moving it: shared_ptr<Player> shared = std::move(unique);. The unique_ptr is left empty and the shared_ptr takes over the object. The reverse isn't possible, because a shared_ptr can't be sure it's the only owner. That's why factory functions often return unique_ptr: callers can keep it unique or turn it into a shared one.

How do I use a smart pointer with a C resource like FILE*?

Give unique_ptr a custom deleter, the function to call instead of delete: unique_ptr<FILE, int(*)(FILE*)> file(fopen("log.txt", "w"), fclose);. When file goes out of scope, fclose runs automatically, even if an exception is thrown. The same trick works for any C library handle that has a matching "close" or "free" function.

C++ Smart Pointers Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the line that creates a heap object owned by a unique_ptr.

auto p = <int>(42);
Multiple choiceeasy

What does a unique_ptr do when it's destroyed?

Predict the outputmedium

What does this print?

auto a = make_shared<int>(1);
auto b = a;
auto c = b;
cout << a.use_count();
Multiple choicemedium

How do you transfer ownership from one unique_ptr to another?

Multiple choicehard

What problem does weak_ptr solve?

C++ Smart Pointers Quiz

5 questions to check what you learned.

  1. Where do objects created with new live?

    • On the heap
    • On the stack
    • In a file
    • In the CPU cache only
  2. What is a memory leak?

    • Reading a file twice
    • A pointer to a stack variable
    • Heap memory that's never freed after it's no longer needed
    • A stack overflow
  3. What does RAII mean in practice?

    • Always use raw pointers
    • A resource is acquired in a constructor and released in the destructor
    • Allocate everything at program start
    • Never use classes
  4. Which smart pointer should you choose by default?

    • shared_ptr
    • weak_ptr
    • unique_ptr
    • auto_ptr
  5. When is an object managed by shared_ptr deleted?

    • When the first shared_ptr is destroyed
    • At the end of main only
    • Never
    • When the last shared_ptr owning it is destroyed or reset