LearnFrenzy
Sign in Take a free test

Memory, Smart Pointers and Move · Lesson 13 of 33

Smart Pointers in C++: unique_ptr and shared_ptr

6 min read Updated Nov 7, 2026 Saurabh Samir

C++ inherits raw pointers and manual memory management from C, with new and delete in place of malloc and free. Forgetting a delete leaks memory; deleting twice crashes. Modern C++ solves this with smart pointers: objects that own heap memory and free it automatically (RAII). In modern code you should almost never write delete.

In this lesson

  • Raw pointers, new and delete – and their problems
  • std::unique_ptr: single ownership
  • std::shared_ptr: shared ownership with reference counting
  • std::weak_ptr and choosing the right pointer

new and delete

#include <iostream>

int main() {
    int* p = new int(42);              // one int on the heap
    int* arr = new int[5]{1, 2, 3, 4, 5};   // an array on the heap

    std::cout << *p << ' ' << arr[4] << '\n';

    delete p;                          // free a single object
    delete[] arr;                      // free an array: must use delete[]
    p = nullptr;                       // nullptr, not NULL, in C++
    return 0;
}

Output

42 5

Why raw new/delete is risky

  • An early return or an exception skips the delete → memory leak.
  • Mixing delete and delete[] is undefined behaviour.
  • Two pointers to the same object → double delete, or a dangling pointer.
  • Nothing in the type says who is responsible for deleting.

For arrays, use std::vector. For single heap objects, use smart pointers.

unique_ptr: one owner

#include <iostream>
#include <memory>
#include <string>
#include <utility>

struct Document {
    std::string title;
    explicit Document(std::string t) : title(std::move(t)) { std::cout << "open " << title << '\n'; }
    ~Document() { std::cout << "close " << title << '\n'; }
};

std::unique_ptr<Document> create(const std::string& name) {
    return std::make_unique<Document>(name);      // ownership moves to the caller
}

int main() {
    auto report = create("report.pdf");
    std::cout << "editing " << report->title << '\n';

    // auto copy = report;                         // error: unique_ptr can't be copied
    auto owner = std::move(report);                // ownership transferred
    std::cout << "report is " << (report ? "set" : "empty") << ", owner has " << owner->title << '\n';

    {
        auto temp = std::make_unique<Document>("temp.txt");
    }                                              // temp closed here automatically
    std::cout << "end of main\n";
    return 0;
}                                                  // owner closed here

Output

open report.pdf
editing report.pdf
report is empty, owner has report.pdf
open temp.txt
close temp.txt
end of main
close report.pdf

unique_ptr has the same size and speed as a raw pointer, but deletes its object when it goes out of scope. It can't be copied – only moved – so there's always exactly one owner. It should be your default choice.

shared_ptr: shared ownership

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Image {
    std::string file;
    explicit Image(std::string f) : file(std::move(f)) {}
    ~Image() { std::cout << "freed " << file << '\n'; }
};

int main() {
    auto logo = std::make_shared<Image>("logo.png");
    std::cout << "owners: " << logo.use_count() << '\n';
    {
        std::vector<std::shared_ptr<Image>> pages{logo, logo, logo};   // three more owners
        std::cout << "owners: " << logo.use_count() << '\n';
    }                                                      // the vector's copies are released
    std::cout << "owners: " << logo.use_count() << '\n';
    logo.reset();                                          // last owner gone → object freed
    std::cout << "done\n";
    return 0;
}

Output

owners: 1
owners: 4
owners: 1
freed logo.png
done

Reference counting

  1. make_sharedcreates the object and a control block with count = 1
  2. each copycount + 1
  3. each destroyed or reset copycount − 1
  4. count reaches 0the object is deleted

weak_ptr: observing without owning

#include <iostream>
#include <memory>

struct Node {
    int value;
    std::shared_ptr<Node> next;        // owns the next node
    std::weak_ptr<Node> prev;          // doesn't own: avoids a reference cycle
    explicit Node(int v) : value(v) {}
    ~Node() { std::cout << "node " << value << " freed\n"; }
};

int main() {
    auto a = std::make_shared<Node>(1);
    auto b = std::make_shared<Node>(2);
    a->next = b;
    b->prev = a;
    if (auto p = b->prev.lock()) {     // lock() gives a shared_ptr if the object still exists
        std::cout << "b's previous node is " << p->value << '\n';
    }
    return 0;
}                                      // both nodes freed: no cycle of shared_ptrs

Output

b's previous node is 1
node 1 freed
node 2 freed

If prev were a shared_ptr, a and b would own each other, their counts would never reach zero, and both would leak. weak_ptr breaks such cycles – common in trees with parent links, caches and observer patterns.

Which pointer?

SituationUse
A sequence of objectsstd::vector<T> (no pointer at all)
One owner of a heap object (polymorphic objects, large objects, factories)std::unique_ptr<T> via make_unique
Several owners, lifetime unknown in advancestd::shared_ptr<T> via make_shared
Refer to a shared object without keeping it alivestd::weak_ptr<T>
A function just uses an object it doesn't ownT&, const T& or a raw T* (non-owning)

Interview angle

Expect "unique_ptr vs shared_ptr", "how does shared_ptr know when to delete?" (reference count in a control block) and "what is a reference cycle and how does weak_ptr fix it?". Also: prefer make_shared/make_unique over new – they're exception-safe and make_shared does one allocation instead of two.

Practice

Try it yourself

  1. Store polymorphic animals in a std::vector<std::unique_ptr<Animal>> and print their sounds.

    Show solution
    #include <iostream>
    #include <memory>
    #include <vector>
    
    struct Animal {
        virtual ~Animal() = default;
        virtual const char* sound() const = 0;
    };
    struct Dog : Animal { const char* sound() const override { return "Woof"; } };
    struct Cat : Animal { const char* sound() const override { return "Meow"; } };
    
    int main() {
        std::vector<std::unique_ptr<Animal>> zoo;
        zoo.push_back(std::make_unique<Dog>());
        zoo.push_back(std::make_unique<Cat>());
        for (const auto& a : zoo) std::cout << a->sound() << '\n';
        return 0;
    }

    Output

    Woof
    Meow

Quick check

Can you copy a unique_ptr?

No – only move it with std::move.

When is a shared_ptr's object deleted?

When the last shared_ptr owning it is destroyed or reset (count reaches 0).

What problem does weak_ptr solve?

Reference cycles between shared_ptrs, which would otherwise leak.

Summary

  • Raw new/delete is error-prone; use vector for arrays and smart pointers for heap objects.
  • unique_ptr = one owner (the default); shared_ptr = reference-counted shared ownership.
  • weak_ptr observes without owning and breaks cycles; create with make_unique/make_shared.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular