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.
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_ptrowns 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_ptris ideal for resources that do not need sharing, such as a single file handle.
1.2 shared_ptr : Resource‑Sharing Coordinator
shared_ptrallows 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_ptrprovides 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_ptrincurs 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
