Smart Pointers in C++
A smart pointer is a wrapper class around a regular (raw) pointer that manages memory automatically. Unlike raw pointers, which require us to manage memory allocation and deallocation manually, a smart pointer automatically frees memory when it is no longer needed. This helps prevent memory leaks and dangling pointers.
The C++ standard provides three kinds of smart pointers in the <memory> header:
unique_ptr— Single owner. Memory is deallocated when theunique_ptrgoes out of scope. It cannot be copied, but it can be moved.shared_ptr— Multiple owners. Uses reference counting to track how manyshared_ptrs point to the same memory. Memory is deallocated when the reference count reaches zero.weak_ptr— Non-owning. It only "observes" the object managed by ashared_ptrwithout increasing the reference count. Useful for avoiding circular references.
Basic principle: "Don't manage memory yourself; let smart pointers handle it." Since C++11, smart pointers have been the standard and are strongly recommended as a replacement for raw pointers in memory management.
Code Example
#include <iostream>
#include <memory>
#include <string>
using namespace std;
// ==================== unique_ptr ====================
class FileHandle {
private:
string filename;
FILE* file;
public:
FileHandle(string name) : filename(name) {
cout << "Opening file: " << filename << endl;
file = fopen(filename.c_str(), "r");
}
~FileHandle() {
if (file) {
fclose(file);
cout << "Closing file: " << filename << endl;
}
}
void read() {
if (file) {
cout << "Reading from file: " << filename << endl;
} else {
cout << "File cannot be opened!" << endl;
}
}
};
// ==================== shared_ptr ====================
class SharedResource {
private:
string name;
public:
SharedResource(string n) : name(n) {
cout << "Resource '" << name << "' created." << endl;
}
~SharedResource() {
cout << "Resource '" << name << "' destroyed." << endl;
}
void use() {
cout << "Using resource: " << name << endl;
}
};
// ==================== weak_ptr ====================
class Parent;
class Child {
public:
string name;
weak_ptr<Parent> parentPtr; // weak_ptr to avoid a circular reference
Child(string n) : name(n) {
cout << "Child '" << name << "' created." << endl;
}
~Child() {
cout << "Child '" << name << "' destroyed." << endl;
}
void showParent() {
auto parent = parentPtr.lock(); // lock() returns a shared_ptr
if (parent) {
cout << "Child '" << name << "' has parent: " << parent->name << endl;
} else {
cout << "Child '" << name << "' has no parent." << endl;
}
}
};
class Parent {
public:
string name;
shared_ptr<Child> childPtr;
Parent(string n) : name(n) {
cout << "Parent '" << name << "' created." << endl;
}
~Parent() {
cout << "Parent '" << name << "' destroyed." << endl;
}
void showChild() {
if (childPtr) {
cout << "Parent '" << name << "' has child: " << childPtr->name << endl;
}
}
};
int main() {
// ==================== unique_ptr ====================
cout << "========== unique_ptr ==========" << endl;
{
unique_ptr<FileHandle> file1 = make_unique<FileHandle>("test.txt");
file1->read();
// unique_ptr cannot be copied
// unique_ptr<FileHandle> file2 = file1; // ERROR!
// But it can be moved
unique_ptr<FileHandle> file2 = move(file1);
if (!file1) {
cout << "file1 is now nullptr after move." << endl;
}
file2->read();
} // file2 is destroyed here, memory is freed automatically
// make_unique (C++14)
cout << "\n=== make_unique ===" << endl;
{
auto ptr1 = make_unique<int>(42);
auto ptr2 = make_unique<string>("Hello");
cout << "*ptr1 = " << *ptr1 << endl;
cout << "*ptr2 = " << *ptr2 << endl;
}
// ==================== shared_ptr ====================
cout << "\n========== shared_ptr ==========" << endl;
{
shared_ptr<SharedResource> sp1 = make_shared<SharedResource>("Database");
cout << "Reference count: " << sp1.use_count() << endl;
{
shared_ptr<SharedResource> sp2 = sp1; // Reference count increases
cout << "Reference count after sharing: " << sp1.use_count() << endl;
sp2->use();
} // sp2 is destroyed, reference count decreases
cout << "Reference count after sp2 is gone: " << sp1.use_count() << endl;
sp1->use();
} // sp1 is destroyed, reference count = 0, resource is destroyed
// make_shared
cout << "\n=== make_shared ===" << endl;
{
auto sp = make_shared<int>(100);
cout << "Value: " << *sp << ", use_count: " << sp.use_count() << endl;
}
// ==================== weak_ptr ====================
cout << "\n========== weak_ptr ==========" << endl;
{
auto parent = make_shared<Parent>("John");
auto child = make_shared<Child>("Peter");
parent->childPtr = child;
child->parentPtr = parent;
parent->showChild();
child->showParent();
cout << "Parent reference count: " << parent.use_count() << endl;
cout << "Child reference count: " << child.use_count() << endl;
}
// ==================== Raw vs Smart Pointer ====================
cout << "\n========== Comparison ==========" << endl;
cout << "Raw pointer: manual new/delete, risk of leak" << endl;
cout << "unique_ptr: automatic cleanup, single ownership, zero overhead" << endl;
cout << "shared_ptr: automatic cleanup, shared ownership, reference counting" << endl;
cout << "weak_ptr: non-owning, breaks circular references" << endl;
return 0;
}
Code Explanation
-
unique_ptr<FileHandle>— A unique pointer that becomes the single owner of the memory. When theunique_ptrgoes out of scope or is destroyed, the pointed-to object is automatically deallocated. -
make_unique<T>(args)— A helper function (C++14) that creates aunique_ptrsafely. It is preferred overunique_ptr<T>(new T(args))because it is more exception-safe. -
move(file1)— Transfers ownership fromfile1tofile2. After the move,file1becomesnullptrbecause it no longer owns anything. -
shared_ptr<SharedResource>— A pointer that can be shared among several owners. Uses reference counting to track the number of owners. -
sp1.use_count()— Returns the number ofshared_ptrs pointing to the same object. The reference count increases when ashared_ptris copied and decreases when it is destroyed. -
make_shared<T>(args)— A helper function that creates the object and theshared_ptrin a single memory allocation. More efficient thanshared_ptr<T>(new T(args)). -
weak_ptr<Parent>— A weak pointer that does not increase the reference count. Used to observe an object without owning it. -
parentPtr.lock()— Returns ashared_ptrfrom aweak_ptr. If the object has already been destroyed,lock()returnsnullptr. -
~FileHandle()— A destructor that closes the file. With aunique_ptr, this destructor is guaranteed to be called when the pointer goes out of scope.
Everyday Use Cases
Imagine you are building a chat application. Each client connection is represented by an object that uses memory resources. With unique_ptr, each client connection is owned by a single thread and destroyed when the connection drops. With shared_ptr, resources needed by many clients (such as an online contacts list) can be shared safely. weak_ptr is used for cross-references between users without creating cycles that prevent memory from being freed.
// Simulating client connection management
auto connection = make_unique<Connection>("192.168.1.1");
// the connection is owned by a single client
// when the client disconnects, the connection is destroyed automatically
// Resource shared across threads
shared_ptr<ContactList> contacts = make_shared<ContactList>();
// contacts can be safely accessed from multiple threads
Conclusion
Smart pointers are the modern C++ solution for automatic memory management. Use unique_ptr as the default for single ownership (lightest and most efficient). Use shared_ptr only when an object really needs to be owned by multiple entities. Use weak_ptr for non-owning references that avoid circular references. Avoid raw pointers for memory management; use smart pointers instead.