Smart Pointers: Understanding Ownership, Memory Overhead and Common Pitfalls
- Sunil Kumar Yadav

- 7 hours ago
- 5 min read

C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do, it blows your whole leg off. — Bjarne Stroustrup
Smart pointers are one of the most useful additions introduced in modern C++. Most developers know that smart pointers help prevent memory leaks by automatically releasing dynamically allocated objects. However, understanding the ownership model and memory overhead of different smart pointers is equally important, especially in embedded and performance-sensitive systems.
In this article, we'll look at:
Why smart pointers exist
Memory overhead of unique_ptr and shared_ptr
Why make_shared() is usually preferred
A dangerous mistake that can lead to double deletion
The Problem with Raw Pointers
Consider the following code to understand key issues with raw pointers.
MyClass* ptr = new MyClass();
// Use object
delete ptr;This looks simple enough. However, there are several problems:
Who owns the object?
Who is responsible for deleting it?
What happens if an exception occurs?
What if the programmer forgets to call delete?
A pointer is a passport to memory. A null pointer is a passport to a country that doesn't exist. A dangling pointer is a passport to a country that used to exist.

Modern C++ addresses these concerns using smart pointers.
unique_ptr - Exclusive Ownership
std::unique_ptr enforces exclusive ownership, meaning only one pointer can manage a specific resource at any given time. It prevents memory leaks and dangling pointers by automatically destroying the managed object and freeing memory when the unique_ptr goes out of scope or is deleted.
auto ptr = std::make_unique<MyClass>();Only one unique_ptr can own the object at a time.
std::unique_ptr<MyClass> ptr1 =
std::make_unique<MyClass>();
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);Ownership is transferred using move semantics. When the owner goes out of scope, the object is automatically destroyed.

How Large is unique_ptr?
Many developers assume that smart pointers introduce significant memory overhead. Let's verify.
#include <iostream>
#include <memory>
int main() {
std::cout << sizeof(int*) << std::endl;
std::cout << sizeof(
std::unique_ptr<int>) << std::endl;
}Typical 64-bit output:
8
8A unique_ptr is usually the same size as a raw pointer. Why? Because internally it only needs to store:
+-----------+
| Raw Pointer |
+-----------+There is no reference counting. There is no control block. The runtime overhead is minimal. This makes unique_ptr the preferred smart pointer whenever shared ownership is not required.
Zero cost abstractions’ can never be an absolute in practice... It means you couldn't hand-write the same abstraction with less costs yourself.
unique_ptr with Custom Deleter
Things become interesting when a custom deleter is introduced. std::unique_ptr accepts a custom deleter as its second template argument, altering how the managed resource is freed when the smart pointer goes out of scope.
auto deleter = [](MyClass* ptr) {
delete ptr;
};
std::unique_ptr<MyClass,
decltype(deleter)> ptr(
new MyClass(),
deleter);Now the smart pointer must store:
+-----------+
| Pointer |
+-----------+
| Deleter |
+-----------+As a result, the size may increase. For stateful deleters, the increase can be even larger. Therefore, if memory footprint matters, it is worth understanding the type and size of custom deleters being used.
shared_ptr - Shared Ownership
A std::shared_ptr is a C++ smart pointer designed for scenarios where multiple owners must collectively manage the lifetime of a single resource. It guarantees the underlying object is only destroyed when the very last owner releases it. A shared_ptr allows multiple owners.
auto ptr1 =
std::make_shared<MyClass>();
auto ptr2 = ptr1;Both pointers now own the same object. The object is destroyed only when the last owner releases it.
ptr1 ----+
|
v
+--------+
| Object |
+--------+
Reference Count = 2Where is the Reference Count Stored?
Many developers expect the reference count to be stored inside the object. It is not.
The reference count is stored in a separate structure called the control block. A typical implementation looks conceptually like:
shared_ptr
+-----------+
| Object Ptr|
+-----------+
Control Block
+------------------+
| Strong Count |
+------------------+
| Weak Count |
+------------------+
| Deleter |
+------------------+
| Allocator |
+------------------+
The exact implementation is library dependent, but the concept remains the same.
How Large is shared_ptr?
We can check this with simple print statement.
std::cout
<< sizeof(std::shared_ptr<int>)
<< std::endl;Typical 64-bit output:
16While a raw pointer occupies:
8and a typical unique_ptr occupies:
8a shared_ptr often occupies:
16The reason for this is because it generally stores:
Pointer to managed object
Pointer to control block
This overhead exists even before considering the control block allocation itself.
Why make_shared() is Recommended
std::make_shared is recommended over using std::shared_ptr with new because it improves performance, ensures exception safety, and simplifies syntax. Consider:
std::shared_ptr<MyClass> ptr(
new MyClass());Most implementations perform:
Two separate heap allocations. Now consider:
auto ptr =
std::make_shared<MyClass>();Many implementations perform:
Allocation #1
+------------------+
| Control Block |
+------------------+
| Object |
+------------------+
A single allocation contains both the control block and the object.
Benefits include:
Fewer heap allocations
Better cache locality
Better performance
Exception safety
For these reasons, make_shared() is usually preferred.
A Dangerous Mistake
Consider the following code.
MyClass* rawPtr =
new MyClass();
std::shared_ptr<MyClass> ptr1(rawPtr);
std::shared_ptr<MyClass> ptr2(rawPtr);At first glance, this may appear harmless. However, it creates two independent control blocks. Conceptually:
ptr1
Control Block A
Ref Count = 1
Object -> rawPtr
ptr2
Control Block B
Ref Count = 1
Object -> rawPtr
Both control blocks believe they are the sole owner of the object. When ptr1 is destroyed:
delete rawPtr;When ptr2 is destroyed:
delete rawPtr;The object is deleted twice. This results in undefined behavior.
Typical symptoms include:
Application crashes
Heap corruption
Random runtime failures
Correct Approach
The correct approach is to avoide using raw pointer to assign to smart pointer. Always create the first owner only once.
auto ptr1 =
std::make_shared<MyClass>();
auto ptr2 = ptr1;Now both smart pointers share the same control block.
Control Block
Ref Count = 2
ptr1 -----+
|
ptr2 -----+
|
v
ObjectThe object is destroyed exactly once when the reference count reaches zero.
shared_ptr with Custom Deleter
If you want to use custom deleter, then we can not use make_shared<>. In such case to pass a custom deleter eg. lambda to a std::shared_ptr, you pass the lambda as the second argument to the std::shared_ptr constructor. Unlike std::unique_ptr, the type of the deleter does not become part of the std::shared_ptr template type
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> ptr(new int(42), [](int* p) {
std::cout << "Custom deleting: " << *p << "\n";
delete p;
});
}Capturing Pitfalls: If your lambda captures a std::shared_ptr by value, you will create a cyclic reference. The reference count will never reach zero, causing a permanent memory leak. If your lambda needs access to a shared resource, capture a std::weak_ptr or raw pointer instead. Let's try to understand this pitfall with simple example of custom deleter, capturing local variable.
#include <memory>
#include <iostream>
std::shared_ptr<int> create_resource() {
int log_counter = 0; // Local stack variable
// CRITICAL BUG:
// Capturing log_counter by reference [&log_counter]
std::shared_ptr<int> ptr(new int(100), [&log_counter](int* p) {
// UNDEFINED BEHAVIOR:
// log_counter no longer exists when this runs!
log_counter++;
std::cout << "Deleted. Total logs: " << log_counter << "\n";
delete p;
});
return ptr;
} // log_counter dies right here
int main() {
auto my_ptr = create_resource();
} // my_ptr dies here, triggers the lambda,
// and accesses garbage memory (crash)Conclusion
In many codebases, unique_ptr should be the default choice, while shared_ptr should be introduced only when multiple owners are required.
Prefer unique_ptr when a single owner exists.
A typical unique_ptr has nearly the same size as a raw pointer.
shared_ptr introduces additional memory and runtime overhead.
shared_ptr requires a control block for reference counting.
Custom deleters and allocators increase the size of the control block.
Prefer make_shared() to reduce allocations and improve efficiency.
Never create multiple shared_ptr instances from the same raw pointer.
Shared ownership should be used only when ownership is genuinely shared.




Comments