Before smart pointers, we must understand where objects live.
Stack Memory
Allocated automatically.
Destroyed automatically.
void foo() {
int x = 10; // stack
std::string s = "Hello"; // stack
}
When foo() exits → x and s are destroyed.
Call foo()
+------------------+
| return address |
|------------------|
| int x = 10 |
|------------------|
| std::string s |
+------------------+
Return → POP → destroyed
Key points:
- No manual cleanup
- Cache friendly
- Deterministic destruction
- Cannot control lifetime beyond scope
Heap Memory
Allocated manually (or via smart pointers).
Lifetime must be managed.
int* p = new int(42);
delete p;
If you forget delete p; → memory leak.
Stack: Heap:
p ---------> +-----------+
| 42 |
+-----------+
Key points:
- Flexible lifetime
- Dynamic size
- Manual delete required
- Easy to leak or double free
Object Lifetime
Object lifetime begins at construction and ends at destruction. On the stack, this is automatic. On the heap, this must be enforced explicitly.
Lifetime = from construction to destruction.
Stack object:
{
std::string s = "Hi";
} // destructor called here
Heap object:
std::string* s = new std::string("Hi");
// destructor NOT called unless delete
That difference is the root of 30+ years of C++ bugs.
Key concept:
- Stack → lifetime tied to scope
- Heap → lifetime tied to ownership logic
RAII — The Core Philosophy
Resource Acquisition Is Initialization
RAII ensures that resource lifetime is tied directly to object lifetime. This is the single most important concept in modern C++ memory safety.
Resource lifetime == object lifetime.
If an object owns a resource:
- Acquire in constructor
- Release in destructor
Example: File wrapper
class File {
FILE* f;
public:
File(const char* name) {
f = fopen(name, "r");
}
~File() {
if (f) fclose(f);
}
};
If an object acquires a resource in its constructor, it must release it in its destructor.
Usage:
void read() {
File file("data.txt");
} // automatically closed here
- No leaks
- Exception safe
- Deterministic cleanup
Key principles:
- Deterministic cleanup
- Exception safety
- No resource leaks
This is the foundation of smart pointers.
Pointers & References
Pointers and references are fundamental language tools, but they do not express ownership clearly by themselves.
Raw Pointers — Why They’re Dangerous
Raw pointers are simply memory addresses. They do not encode ownership rules.
int* p = new int(5);
delete p;
*p = 10; // Undefined behavior (dangling pointer)
Problems
- Memory leaks
- Double delete
- Dangling pointer
- Ownership unclear
- Hard to reason in multithreaded systems
Dangling Pointer Diagram
delete p;
p ------X-----> freed memory
In embedded / secure systems → this becomes a vulnerability.
Problems:
- Memory leaks
- Dangling pointers
- Double delete
- Ownership ambiguity
- Hard to reason in multithreaded systems
In security-sensitive systems, use-after-free becomes an exploit vector.
References — Why They’re Limited
References are safer than raw pointers because they cannot be null and cannot be reseated.
int x = 10;
int& ref = x;
- Cannot be null
- Cannot be reseated
But:
- No ownership
- Cannot represent optional object
- Cannot manage dynamic lifetime
References are aliases, not owners.
Smart Pointers
Smart pointers implement RAII for heap memory.
Smart pointers bring RAII to heap memory. They encode ownership directly into the type system, eliminating entire classes of memory bugs.
They answer explicitly:
- Who owns this object?
- When is it destroyed?
- Can ownership be shared?
std::unique_ptr
Exclusive ownership. Only one owner exists.
#include <memory>
std::unique_ptr<int> p = std::make_unique<int>(42);
Ownership Rule
unique_ptr ----> object
If unique_ptr dies → object dies.
Move Semantics
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1);
After move:
p1 = null
p2 ----> object
- Zero overhead
- Deterministic
- Safe
- Best default choice
std::shared_ptr
Shared ownership. Reference counted.
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p1;
Reference Count Diagram
p1 \
----> object (count = 2)
p2 /
When count reaches 0 → object destroyed.
Cost
- Control block
- Atomic ref counting
- More memory
- Slower
In high-performance systems → use carefully.
std::weak_ptr
Non-owning observer of shared_ptr. Prevents circular references.
std::weak_ptr<int> wp = p1;
if (auto sp = wp.lock()) {
std::cout << *sp;
}
Weak pointer does not increase ref count.
Key points:
- Breaks cycles
- Safe access via lock()
- Slight runtime overhead
The Circular Reference Problem
struct B;
struct A {
std::shared_ptr<B> b;
};
struct B {
std::shared_ptr<A> a;
};
Graph:
A <----> B
Ref count never reaches zero → memory leak.
Fix with weak_ptr
struct B {
std::weak_ptr<A> a;
};
Now:
A ----> B
^ |
| v
weak shared
Full Example — Proper Ownership Design
#include <memory>
#include <iostream>
class Sensor {
public:
void read() { std::cout << "Reading\n"; }
};
class Controller {
std::unique_ptr<Sensor> sensor;
public:
Controller()
: sensor(std::make_unique<Sensor>()) {}
void process() {
sensor->read();
}
};
int main() {
Controller c;
c.process();
}
Ownership graph:
Controller
|
+---- unique_ptr ----> Sensor
Clear. Deterministic. Safe.
Summary
Stack → automatic lifetime
Heap → explicit ownership required
RAII → lifetime == scope
Smart pointers → RAII for heap