C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe Patterns
이 글의 핵심
Hands-on guide to C++ smart pointers—unique_ptr, shared_ptr, weak_ptr—with examples and real-world patterns.
Introduction
Smart pointers are RAII-style wrappers that provide automatic memory management. They help avoid leaks and dangling pointers from raw new/delete.
// ❌ Raw pointer (unsafe)
int* ptr = new int(10); // Allocate on heap
// ... use ...
delete ptr; // Manual delete (forget it → leak!)
// Issues:
// 1. Forgetting delete → leak
// 2. Exception may skip delete
// 3. Double delete → crash
// 4. Use after delete → UB (dangling)
// ✅ Smart pointer (safe)
// std::make_unique: factory for unique_ptr
// RAII: acquire in constructor, release in destructor
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// delete runs automatically at end of scope
// Still safe if exceptions occur
1. unique_ptr — exclusive ownership
Basic usage
#include <memory>
#include <iostream>
int main() {
// Create
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// Use
std::cout << *ptr << std::endl; // 10
*ptr = 20;
std::cout << *ptr << std::endl; // 20
// nullptr check
if (ptr) {
std::cout << "valid" << std::endl;
}
// Array
std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
arr[0] = 1;
arr[1] = 2;
std::cout << arr[0] << ", " << arr[1] << std::endl; // 1, 2
return 0;
} // automatic delete
Move only (no copy)
#include <memory>
#include <iostream>
// Pass by value: transfer ownership
// Memory freed when function returns
void process(std::unique_ptr<int> ptr) {
std::cout << "value: " << *ptr << std::endl;
} // ptr destroyed → memory freed
int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
// ❌ No copy: unique_ptr is exclusive
// std::unique_ptr<int> ptr2 = ptr1; // compile error
// Copy constructor is deleted
// ✅ Move: transfer ownership with std::move
// Ownership moves from ptr1 to ptr2
// After move, ptr1 is nullptr
std::unique_ptr<int> ptr2 = std::move(ptr1);
// Check ptr1
if (!ptr1) {
std::cout << "ptr1 is nullptr" << std::endl;
}
// Check ptr2
if (ptr2) {
std::cout << "ptr2 valid: " << *ptr2 << std::endl;
}
// Pass to function: transfer ownership
// std::move(ptr2) transfers ownership into process
// After call, ptr2 is nullptr
process(std::move(ptr2));
if (!ptr2) {
std::cout << "ptr2 also nullptr" << std::endl;
}
return 0;
}
Output:
ptr1 is nullptr
ptr2 valid: 10
value: 10
ptr2 also nullptr
2. shared_ptr — shared ownership
Basic usage
#include <memory>
#include <iostream>
int main() {
// std::make_shared: preferred way to make shared_ptr
// One allocation for control block + object (efficient)
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// use_count(): current reference count
std::cout << "ref count: " << ptr1.use_count() << std::endl; // 1
{
// shared_ptr copies: ref count increases
// ptr1 and ptr2 share the same object
std::shared_ptr<int> ptr2 = ptr1; // copy ok
// Ref count 2: two owners
std::cout << "ref count: " << ptr1.use_count() << std::endl; // 2
std::cout << "ptr1: " << *ptr1 << std::endl; // 10
std::cout << "ptr2: " << *ptr2 << std::endl; // 10
} // ptr2 destroyed → count 2 → 1
// ptr1 still alive → storage kept
std::cout << "ref count: " << ptr1.use_count() << std::endl; // 1
return 0;
} // ptr1 destroyed → count 0 → freed
Output:
ref count: 1
ref count: 2
ptr1: 10
ptr2: 10
ref count: 1
Reference counting
#include <memory>
#include <iostream>
#include <vector>
class Resource {
public:
Resource(int id) : id_(id) {
std::cout << "Resource " << id_ << " created" << std::endl;
}
~Resource() {
std::cout << "Resource " << id_ << " destroyed" << std::endl;
}
int getId() const { return id_; }
private:
int id_;
};
int main() {
std::vector<std::shared_ptr<Resource>> resources;
{
auto r1 = std::make_shared<Resource>(1);
resources.push_back(r1);
resources.push_back(r1);
resources.push_back(r1);
std::cout << "ref count: " << r1.use_count() << std::endl; // 4
} // r1 gone but vector still holds refs
std::cout << "vector size: " << resources.size() << std::endl; // 3
std::cout << "ref count: " << resources[0].use_count() << std::endl; // 3
resources.clear(); // drop refs → destroy Resource
return 0;
}
Output:
Resource 1 created
ref count: 4
vector size: 3
ref count: 3
Resource 1 destroyed
3. weak_ptr — breaking circular references
The circular reference problem
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a_ptr; // circular ref!
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // circular reference
std::cout << "a ref count: " << a.use_count() << std::endl; // 2
std::cout << "b ref count: " << b.use_count() << std::endl; // 2
} // a,b dtor never runs — not freed!
std::cout << "block end" << std::endl;
return 0;
}
Output:
a ref count: 2
b ref count: 2
block end
Problem: A and B destructors never run (leak).
Fixing it with weak_ptr
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // use weak_ptr
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr does not bump strong count
std::cout << "a ref count: " << a.use_count() << std::endl; // 1
std::cout << "b ref count: " << b.use_count() << std::endl; // 2
} // A and B destroy normally
std::cout << "block end" << std::endl;
return 0;
}
Output:
a ref count: 1
b ref count: 2
B destroyed
A destroyed
block end
Using weak_ptr
#include <memory>
#include <iostream>
int main() {
// weak_ptr: non-owning weak reference
std::weak_ptr<int> weak;
{
// create shared_ptr
std::shared_ptr<int> shared = std::make_shared<int>(42);
// assign to weak_ptr: no strong count bump
// does not extend shared lifetime
weak = shared;
// strong count still 1
std::cout << "shared ref count: " << shared.use_count() << std::endl; // 1
// use weak_ptr: get shared_ptr via lock()
// lock(): shared if alive, else nullptr
if (auto locked = weak.lock()) {
// locked: temporary shared (count++)
std::cout << "value: " << *locked << std::endl; // 42
// ref count 2: shared + locked
std::cout << "ref count: " << locked.use_count() << std::endl; // 2
} // locked destroyed → back to 1
} // shared gone → count 0 → freed
// check weak_ptr expiry
// expired(): was object destroyed?
if (weak.expired()) {
std::cout << "weak_ptr expired" << std::endl;
}
// lock() on expired weak_ptr
if (auto locked = weak.lock()) {
std::cout << "value: " << *locked << std::endl;
} else {
// object gone — lock fails
std::cout << "lock failed" << std::endl;
}
return 0;
}
Output:
shared ref count: 1
value: 42
ref count: 2
weak_ptr expired
lock failed
4. Practical examples
Example 1: Resource management
#include <memory>
#include <iostream>
#include <fstream>
class FileHandler {
private:
std::unique_ptr<std::ofstream> file;
std::string filename;
public:
FileHandler(const std::string& filename) : filename(filename) {
file = std::make_unique<std::ofstream>(filename);
if (!file->is_open()) {
throw std::runtime_error("failed to open file: " + filename);
}
std::cout << "file opened: " << filename << std::endl;
}
~FileHandler() {
if (file && file->is_open()) {
file->close();
std::cout << "file closed: " << filename << std::endl;
}
}
void write(const std::string& data) {
if (file && file->is_open()) {
*file << data << std::endl;
}
}
};
int main() {
try {
FileHandler handler("output.txt");
handler.write("Hello");
handler.write("World");
// file still closed on exception
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << std::endl;
}
return 0;
}
Example 2: Factory pattern
#include <memory>
#include <iostream>
#include <string>
class Animal {
public:
virtual void speak() = 0;
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
~Dog() {
std::cout << "Dog destroyed" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
~Cat() {
std::cout << "Cat destroyed" << std::endl;
}
};
std::unique_ptr<Animal> createAnimal(const std::string& type) {
if (type == "dog") {
return std::make_unique<Dog>();
} else if (type == "cat") {
return std::make_unique<Cat>();
}
return nullptr;
}
int main() {
auto animal1 = createAnimal("dog");
if (animal1) {
animal1->speak();
}
auto animal2 = createAnimal("cat");
if (animal2) {
animal2->speak();
}
auto animal3 = createAnimal("bird");
if (!animal3) {
std::cout << "unknown animal" << std::endl;
}
return 0;
}
Output:
Woof!
Meow!
unknown animal
Cat destroyed
Dog destroyed
Example 3: Cache (shared_ptr)
#include <memory>
#include <unordered_map>
#include <iostream>
#include <string>
class Resource {
private:
std::string name;
public:
Resource(std::string n) : name(n) {
std::cout << "load resource: " << name << std::endl;
}
~Resource() {
std::cout << "unload resource: " << name << std::endl;
}
void use() {
std::cout << name << " in use" << std::endl;
}
std::string getName() const { return name; }
};
class ResourceCache {
private:
std::unordered_map<std::string, std::shared_ptr<Resource>> cache;
public:
std::shared_ptr<Resource> getResource(const std::string& name) {
if (cache.find(name) == cache.end()) {
cache[name] = std::make_shared<Resource>(name);
}
return cache[name];
}
void printCacheSize() {
std::cout << "cache size: " << cache.size() << std::endl;
}
void clear() {
cache.clear();
std::cout << "cache cleared" << std::endl;
}
};
int main() {
ResourceCache cache;
{
auto r1 = cache.getResource("texture1");
auto r2 = cache.getResource("texture1"); // same object
r1->use();
std::cout << "r1 ref count: " << r1.use_count() << std::endl; // 3 (r1, r2, cache)
std::cout << "r2 ref count: " << r2.use_count() << std::endl; // 3
} // r1,r2 gone but cache holds ref
cache.printCacheSize(); // 1
cache.clear(); // clear cache → unload
return 0;
}
Output:
load resource: texture1
texture1 in use
r1 ref count: 3
r2 ref count: 3
cache size: 1
cache cleared
unload resource: texture1
5. Common pitfalls
Pitfall 1: Avoiding make_unique / make_shared
#include <memory>
void func(std::unique_ptr<int> p1, std::unique_ptr<int> p2) {
// ...
}
int main() {
// ❌ unsafe: exception safety issue
// func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
// evaluation order not guaranteed → possible leak:
// 1. new int(1)
// 2. new int(2)
// 3. unique_ptr ctor (exception can leak 1,2)
// ✅ safe
func(std::make_unique<int>(1), std::make_unique<int>(2));
// ✅ or
auto p1 = std::make_unique<int>(1);
auto p2 = std::make_unique<int>(2);
func(std::move(p1), std::move(p2));
return 0;
}
Pitfall 2: shared_ptr cycles
#include <memory>
#include <iostream>
// ❌ cycle
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // cycle!
int value;
Node(int v) : value(v) {
std::cout << "Node " << value << " created" << std::endl;
}
~Node() {
std::cout << "Node " << value << " destroyed" << std::endl;
}
};
void testCircular() {
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // circular ref — dtors never run
std::cout << "n1 ref count: " << n1.use_count() << std::endl; // 2
std::cout << "n2 ref count: " << n2.use_count() << std::endl; // 2
}
// ✅ use weak_ptr
class NodeFixed {
public:
std::shared_ptr<NodeFixed> next;
std::weak_ptr<NodeFixed> prev; // weak_ptr
int value;
NodeFixed(int v) : value(v) {
std::cout << "NodeFixed " << value << " created" << std::endl;
}
~NodeFixed() {
std::cout << "NodeFixed " << value << " destroyed" << std::endl;
}
};
void testFixed() {
auto n1 = std::make_shared<NodeFixed>(1);
auto n2 = std::make_shared<NodeFixed>(2);
n1->next = n2;
n2->prev = n1; // weak_ptr does not bump strong count
std::cout << "n1 ref count: " << n1.use_count() << std::endl; // 1
std::cout << "n2 ref count: " << n2.use_count() << std::endl; // 2
}
int main() {
std::cout << "=== circular ref test ===" << std::endl;
testCircular();
std::cout << "end function (dtors NOT called!)" << std::endl;
std::cout << "\n=== weak_ptr test ===" << std::endl;
testFixed();
std::cout << "end function (dtors called)" << std::endl;
return 0;
}
Output:
=== circular ref test ===
Node 1 created
Node 2 created
n1 ref count: 2
n2 ref count: 2
end function (dtors NOT called!)
=== weak_ptr test ===
NodeFixed 1 created
NodeFixed 2 created
n1 ref count: 1
n2 ref count: 2
NodeFixed 2 destroyed
NodeFixed 1 destroyed
end function (dtors called)
Pitfall 3: Passing unique_ptr to functions
#include <memory>
#include <iostream>
// Option 1: transfer ownership
void takeOwnership(std::unique_ptr<int> ptr) {
std::cout << "ownership transfer: " << *ptr << std::endl;
}
// Option 2: pass by const& (keep ownership)
void borrow(const std::unique_ptr<int>& ptr) {
std::cout << "borrow: " << *ptr << std::endl;
}
// Option 3: raw pointer (non-owning)
void observe(int* ptr) {
if (ptr) {
std::cout << "observe: " << *ptr << std::endl;
}
}
int main() {
auto ptr = std::make_unique<int>(10);
// ❌ compile error
// takeOwnership(ptr); // no copy
// ✅ transfer ownership
// takeOwnership(std::move(ptr)); // ptr becomes nullptr
// ✅ pass by const&
borrow(ptr); // ptr still valid
// ✅ raw .get()
observe(ptr.get()); // ptr still valid
std::cout << "ptr valid: " << (ptr ? "yes" : "no") << std::endl;
return 0;
}
Output:
borrow: 10
observe: 10
ptr valid: yes
6. Example: resource manager
#include <memory>
#include <vector>
#include <iostream>
#include <string>
class ResourceManager {
private:
std::vector<std::unique_ptr<std::string>> resources_;
public:
// add resource
void add(std::unique_ptr<std::string> resource) {
resources_.push_back(std::move(resource));
}
// create and add
void create(const std::string& value) {
resources_.push_back(std::make_unique<std::string>(value));
}
// take (move out)
std::unique_ptr<std::string> take(size_t index) {
if (index >= resources_.size()) return nullptr;
auto resource = std::move(resources_[index]);
resources_.erase(resources_.begin() + index);
return resource;
}
// count
size_t count() const {
return resources_.size();
}
// print
void print() const {
std::cout << "Resources (" << resources_.size() << "):" << std::endl;
for (size_t i = 0; i < resources_.size(); ++i) {
if (resources_[i]) {
std::cout << " [" << i << "]: " << *resources_[i] << std::endl;
} else {
std::cout << " [" << i << "]: (moved)" << std::endl;
}
}
}
};
int main() {
ResourceManager mgr;
// add resource
mgr.add(std::make_unique<std::string>("Resource 1"));
mgr.create("Resource 2");
mgr.create("Resource 3");
std::cout << "initial state:" << std::endl;
mgr.print();
// take resource
auto r = mgr.take(1);
std::cout << "\ntook resource: " << *r << std::endl;
std::cout << "\nremaining:" << std::endl;
mgr.print();
return 0;
}
Output:
initial state:
Resources (3):
[0]: Resource 1
[1]: Resource 2
[2]: Resource 3
took resource: Resource 2
remaining:
Resources (2):
[0]: Resource 1
[1]: Resource 3
Summary
Key takeaways
- unique_ptr: exclusive; no copy; movable
- shared_ptr: shared ownership; ref counting
- weak_ptr: break cycles; no strong count bump
- make_unique/make_shared: exception safety; performance
- RAII: automatic cleanup
Smart pointer comparison
| Feature | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| Ownership | exclusive | shared | none |
| Copy | no | yes | yes |
| Move | yes | yes | yes |
| Overhead | none | ref count | none |
| Use | default | sharing | cycles |
| Arrays | yes (T[]) | limited | - |
Practical tips
Selection guide:
- Default:
unique_ptr - Sharing:
shared_ptr - Cycles:
weak_ptr - Arrays:
unique_ptr<T[]>orvector
Performance:
unique_ptr: same cost as raw pointershared_ptr: small refcount overheadmake_shared: often one allocation
Caveats:
- Prefer
make_unique/make_shared - Watch for cycles
- Do not use moved-from objects
- Avoid dangling pointers
Next steps
- C++ RAII
- C++ Move Semantics
- C++ weak_ptr
Related posts (internal links)
More articles connected to this topic.
- C++ smart pointer basics | unique_ptr & shared_ptr
- C++ RAII & smart pointers
- C++ smart pointers & circular references [#33-3]
Practical tips
Tips you can apply at work.
Debugging
- Check compiler warnings first
- Reproduce with a minimal test
Performance
- Do not optimize without profiling
- Define measurable goals first
Code review
- Anticipate typical review feedback
- Follow team conventions
Practical checklist
Use this when applying these ideas in production.
Before you code
- Is this the best fix for the problem?
- Can teammates maintain this?
- Meets performance requirements?
While coding
- All warnings fixed?
- Edge cases covered?
- Error handling OK?
At review
- Intent clear?
- Tests sufficient?
- Documented?
Use this checklist to reduce mistakes and improve quality.
Keywords (search)
C++, smart pointers, unique_ptr, shared_ptr, memory management — searches like these should help you find this article.
Related posts
- C++ shared_ptr vs unique_ptr |
- C++ smart pointer basics | unique_ptr & shared_ptr
- C++ unique_ptr advanced | custom deleters & arrays
- C++ circular references | shared_ptr leaks
- C++ RAII & Smart Pointers |