Fundamentals 24 min read

Mastering C++ Smart Pointers: unique_ptr, shared_ptr, and weak_ptr Explained

This article introduces C++ smart pointers—unique_ptr, shared_ptr, and weak_ptr—explaining their RAII‑based automatic memory management, usage patterns, performance considerations, common pitfalls such as cyclic references, and practical examples including factory patterns and GUI data sharing, while also covering size and custom deleters.

Deepin Linux
Deepin Linux
Deepin Linux
Mastering C++ Smart Pointers: unique_ptr, shared_ptr, and weak_ptr Explained

In C++ programming, memory management is a critical and tricky issue. Traditional manual management using new and delete can easily cause leaks and dangling pointers. C++ introduces smart pointers to solve these problems.

Smart pointers encapsulate raw pointers and use the RAII (Resource Acquisition Is Initialization) mechanism to automatically release managed memory when the smart‑pointer object goes out of scope, eliminating manual management risks. The standard library provides several types, the most common being unique_ptr, shared_ptr, and weak_ptr.

1. Smart Pointers Arrive: The Timely Rain

Smart pointers act like a diligent steward that handles memory chores, allowing developers to focus on core logic. They rely on RAII: acquire a resource at construction and release it automatically at destruction, similar to renting a house and leaving it when the lease ends.

1.1 unique_ptr : Exclusive Memory Guardian

unique_ptr

owns an object exclusively; only one unique_ptr can point to it. When the unique_ptr goes out of scope, the object is destroyed and memory freed.

#include <iostream>
#include <memory>

int main() {
    // Create a unique_ptr to an int
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    std::cout << "Value: " << *ptr << std::endl;
    // ptr leaves scope, the int is automatically destroyed
    return 0;
}
unique_ptr

is ideal for resources that do not need sharing, such as a single file handle.

1.2 shared_ptr : Resource‑Sharing Coordinator

shared_ptr

allows multiple pointers to share ownership of the same object. It maintains a reference count; the object is destroyed only when the count drops to zero.

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::cout << "ptr1 count: " << ptr1.use_count() << std::endl;
    std::shared_ptr<MyClass> ptr2 = ptr1;
    std::cout << "ptr1 count: " << ptr1.use_count() << std::endl;
    std::cout << "ptr2 count: " << ptr2.use_count() << std::endl;
    return 0;
}

This enables safe shared access without manual deallocation.

1.3 weak_ptr : The Secret Weapon Against Cyclic References

weak_ptr

provides a non‑owning reference to an object managed by shared_ptr. It does not increase the reference count, thus breaking potential reference cycles.

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destructor" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_weak;
    ~B() { std::cout << "B destructor" << std::endl; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_weak = a; // does not increase A's reference count
    return 0;
}

When needed, weak_ptr can be locked to obtain a temporary shared_ptr for safe access.

2. The Three Brothers of Smart Pointers

2.1 unique_ptr : Exclusive Owner

Example showing transfer of ownership with std::move and automatic destruction.

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
    void print() { std::cout << "Printing" << std::endl; }
};

int main() {
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
    ptr1->print();
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
    if (!ptr1) std::cout << "ptr1 is null" << std::endl;
    return 0;
}

2.2 shared_ptr : Shared Coordinator

Demonstrates reference counting and automatic cleanup.

2.3 weak_ptr : Breaking Cycles

Shows how weak_ptr prevents memory leaks caused by circular references.

3. Practical Applications

3.1 Case Study: Resource Management in a Factory Pattern

Using unique_ptr to manage game character objects created by a factory.

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

class Character { public: virtual void display() const = 0; virtual ~Character() = default; };
class Warrior : public Character { public: void display() const override { std::cout << "I am a Warrior" << std::endl; } };
class Mage : public Character { public: void display() const override { std::cout << "I am a Mage" << std::endl; } };
class Assassin : public Character { public: void display() const override { std::cout << "I am an Assassin" << std::endl; } };

class CharacterFactory {
public:
    static std::unique_ptr<Character> createCharacter(const std::string& type) {
        if (type == "warrior") return std::make_unique<Warrior>();
        if (type == "mage") return std::make_unique<Mage>();
        if (type == "assassin") return std::make_unique<Assassin>();
        return nullptr;
    }
};

int main() {
    auto warrior = CharacterFactory::createCharacter("warrior");
    if (warrior) warrior->display();
    auto mage = CharacterFactory::createCharacter("mage");
    if (mage) mage->display();
    return 0;
}

3.2 Case Study: Data Sharing in GUI Programming

Uses shared_ptr to share a GraphicData object between a main window and a property window.

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

class GraphicData { public: std::string name; int width; int height; GraphicData(const std::string& n,int w,int h):name(n),width(w),height(h){} };
class MainWindow { public: std::shared_ptr<GraphicData> data; MainWindow(const std::shared_ptr<GraphicData>& d):data(d){} void display(){ std::cout << "Main window shows: " << data->name << ", " << data->width << "x" << data->height << std::endl; } };
class PropertyWindow { public: std::shared_ptr<GraphicData> data; PropertyWindow(const std::shared_ptr<GraphicData>& d):data(d){} void update(int w,int h){ data->width=w; data->height=h; std::cout << "Property window updated: " << data->width << "x" << data->height << std::endl; } };

int main() {
    auto graphicData = std::make_shared<GraphicData>("Rectangle",100,200);
    MainWindow mainWin(graphicData);
    PropertyWindow propWin(graphicData);
    mainWin.display();
    propWin.update(150,250);
    mainWin.display();
    return 0;
}

4. Pitfall Guide

4.1 Performance Considerations

shared_ptr

incurs overhead due to atomic reference‑count updates and an extra control block. In performance‑critical code, unique_ptr is often preferable.

4.2 Avoiding Cyclic References

When objects reference each other via shared_ptr, use weak_ptr to break the cycle and allow proper destruction.

4.3 Proper Initialization and Assignment

Prefer std::make_unique and std::make_shared over direct new to avoid fragmentation and improve exception safety.

// Preferred
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::shared_ptr<int> p2 = std::make_shared<int>(42);

// Not recommended
std::unique_ptr<int> p3(new int(42));
std::shared_ptr<int> p4(new int(42));

5. Size of unique_ptr

On a 64‑bit system, a plain unique_ptr without a custom deleter occupies the same 8 bytes as a raw pointer.

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int(10));
    std::cout << "sizeof(std::unique_ptr<int>) = " << sizeof(ptr) << std::endl;
    int* rawPtr = new int(10);
    std::cout << "sizeof(int*) = " << sizeof(rawPtr) << std::endl;
    delete rawPtr;
    return 0;
}

When a custom deleter is added, the size may increase. For example, a function‑pointer deleter adds another 8 bytes, making the total 16 bytes, while a stateless lambda often incurs no extra size.

#include <iostream>
#include <memory>

void customDeleter(int* p) { std::cout << "Custom deleter" << std::endl; delete p; }

int main() {
    std::unique_ptr<int, void(*)(int*)> ptr(new int(10), customDeleter);
    std::cout << "sizeof(std::unique_ptr<int, void(*)(int*)>) = " << sizeof(ptr) << std::endl;
    return 0;
}

Using a stateless lambda as deleter typically keeps the size at 8 bytes because the compiler can optimize away the empty closure.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Memory ManagementCshared_ptrsmart pointersunique_ptrweak_ptr
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.