C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe
이 글의 핵심
C++ smart pointers explained: unique_ptr for exclusive ownership, shared_ptr for shared ownership, weak_ptr for cycles—examples, make_unique/make_shared, and production tips.
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[]>orvectorPerformance: 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
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. C++ smart pointers explained: unique_ptr for exclusive ownership, shared_ptr for shared ownership, weak_ptr for cycles—e…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ weak_ptr | ‘약한 포인터’ 가이드
- C++ RAII & Smart Pointers | ‘스마트 포인터’ 가이드
- C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
Keywords Covered in This Article (Related Search Terms)
This article covers C++, smart pointers, unique_ptr, shared_ptr, memory management, RAII.