Understanding std::shared_ptr
std::shared_ptr
is a smart pointer that provides shared ownership of dynamically allocated objects. Unlike std::unique_ptr
, which enforces exclusive ownership, std::shared_ptr
instances to share ownership of the same resource. It employs a reference counting mechanism to keep track of the number of std::shared_ptr
instances pointing to a particular object automatically deallocates the memory when the last shared pointer goes out of scope.
Syntax and Features
Declaration and Initialization:
#include <memory>
std::shared_ptr<int> ptr1 = std::make_shared<int>(42); // Preferred way
std::shared_ptr<int> ptr2(new int(42)); // Allowed, but avoid using raw new
The std::make_shared
function is preferred for creating a std::shared_ptr
as it ensures both memory allocation and the creation of the managed object occur together, enhancing performance.
Use std::make_shared
whenever possible. It’s more efficient and exception-safe, as it creates the object and the control block (reference count) in one memory allocation.
Features
1 Shared Ownership:
std::shared_ptr
allows multiple pointers to share ownership of the same dynamically allocated object. Each std::shared_ptr
instance maintains a reference count, and the object is automatically deallocated when the last shared pointer pointing to it goes out of scope.
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1; // Both share ownership
std::cout << *ptr1 << ", " << *ptr2 << std::endl; // Both access the same object
Both ptr1
and ptr2
point to the same memory, and the resource will only be released when both are destroyed or reset.
Example:
#include <memory>
#include <iostream>
int main() {
// Creating a shared_ptr to manage dynamically allocated memory
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // Shared ownership
// Accessing the managed object through ptr2
std::cout << *ptr2 << std::endl; // Output: 42
// No need to explicitly delete memory, handled by shared_ptr
return 0;
}
2 Reference Counting
std::shared_ptr
uses reference counting to keep track of the number of shared pointers pointing to the same object. When a shared pointer is copied or destroyed, the reference count is updated accordingly.
You can check how many shared_ptr
instances are sharing the resource using .use_count()
:
std::cout << "Use count: " << ptr1.use_count() << std::endl;
Example:
#include <memory>
#include <iostream>
int main() {
// Creating a shared_ptr to manage dynamically allocated memory
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // Shared ownership
// Displaying the reference count
std::cout << "Reference count: " << ptr1.use_count() << std::endl; // Output: 2
return 0;
}
3 Automatic Memory Management
std::shared_ptr
automatically deallocates the associated memory when the last shared pointer pointing to the object goes out of scope. This ensures proper cleanup and prevents memory leaks.
Example:
#include <memory>
#include <iostream>
void func() {
std::shared_ptr<int> ptr = std::make_shared<int>(42);
} // ptr goes out of scope and memory is automatically deallocated
int main() {
func(); // Memory deallocated after func() returns
std::cout << "Memory deallocated successfully" << std::endl;
return 0;
}
4 Control Block
std::shared_ptr
uses a control block to store the reference count and manage shared ownership of the object. Each std::shared_ptr
instance points to the control block, which in turn points to the dynamically allocated object.
Example:
#include <memory>
#include <iostream>
int main() {
// Creating a shared_ptr to manage dynamically allocated memory
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // Shared ownership
// Displaying the address of the control block
std::cout << "Control block address: " << ptr1.get() << std::endl;
return 0;
}
// Output
Control block address: 0x131a2c0
5 Resetting and Releasing
You can manually release ownership using reset()
:
ptr1.reset(); // Decreases the reference count
Note: shared_ptr
does not have a release()
method like unique_ptr
— this is intentional, to avoid unsafe use of raw pointers.
Custom Deleters with shared_ptr
Like unique_ptr
, shared_ptr
also supports custom deleters, allowing safe cleanup for non-memory resources:
auto fileCloser = [](FILE* f) {
if (f) fclose(f);
};
std::shared_ptr<FILE> file(fopen("log.txt", "r"), fileCloser);
This ensures fclose
is called when the last shared_ptr
owning the file is destroyed.
Example Usage
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> a = std::make_shared<int>(100);
std::shared_ptr<int> b = a;
std::cout << "Value: " << *a << std::endl;
std::cout << "Use count: " << a.use_count() << std::endl;
b.reset();
std::cout << "Use count after reset: " << a.use_count() << std::endl;
return 0;
}
Output:
Value: 100
Use count: 2
Use count after reset: 1
Benefits of std::shared_ptr
Feature | Description |
---|---|
🤝 Shared Ownership | Multiple smart pointers can manage the same resource. |
🔁 Automatic Cleanup | Deletes the object only when all shared owners are gone. |
🧠 Reference Counting | Tracks how many owners are sharing the object. |
🔧 Custom Deleters | Cleanly manage files, sockets, or other resources. |
💡 Thread-Safe Reference Counting | Safe to use across threads (object access still needs synchronization). |
When Not to Use shared_ptr
- When you don’t need shared ownership — prefer
unique_ptr
to avoid overhead. - In cyclic dependencies (e.g., two
shared_ptr
s pointing to each other) — this causes memory leaks. Usestd::weak_ptr
to break the cycle.
Problem with shared_ptr
1 Cyclic (Circular) References - Memory Leaks
The most notorious issue with shared_ptr
is that cyclic references never get freed. If two or more shared_ptr
s reference each other, their reference counts never drop to zero, even when they go out of scope.
struct B;
struct A {
std::shared_ptr<B> b;
};
struct B {
std::shared_ptr<A> a;
};
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a; // ❌ Memory leak — circular reference
2. 🐢 Performance Overhead
shared_ptr
uses a control block that tracks reference counts. This adds:
Memory overhead (the control block itself)
CPU overhead (atomic operations on the reference counter, which are thread-safe but slower)
This makes shared_ptr
heavier than unique_ptr
, especially in tight loops or performance-critical systems.
3 Incorrect Use of new
Instead of make_shared
Manually calling new
and passing the raw pointer to a shared_ptr
is error-prone and less efficient:
std::shared_ptr<MyClass> p(new MyClass); // ❌ okay but suboptimal
Issues:
You may accidentally create two separate
shared_ptr
s managing the same raw pointer — leading to double deletion.It performs two allocations: one for the object, one for the control block.
Solution:
Use std::make_shared
:
auto p = std::make_shared<MyClass>(); // ✅ safer and more efficient
4. ❌ Dangling Shared Pointers
If a shared_ptr
is constructed from a raw pointer that is also managed elsewhere, or from a local stack variable, it may lead to undefined behavior or double deletion.
int* x = new int(5);
std::shared_ptr<int> a(x);
std::shared_ptr<int> b(x); // ❌ Two shared_ptrs managing the same raw pointer
Solution:
Avoid directly using new
unless absolutely necessary. Prefer make_shared
, and never pass the same raw pointer to multiple shared_ptrs.