[2026] C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe Patterns
이 글의 핵심
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.
다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
ptr1 is nullptr
ptr2 valid: 10
value: 10
ptr2 also nullptr
2. shared_ptr — shared ownership
Basic usage
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
ref count: 1
ref count: 2
ptr1: 10
ptr2: 10
ref count: 1
Reference counting
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
a ref count: 1
b ref count: 2
B destroyed
A destroyed
block end
Using weak_ptr
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
shared ref count: 1
value: 42
ref count: 2
weak_ptr expired
lock failed
4. Practical examples
Example 1: Resource management
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Woof!
Meow!
unknown animal
Cat destroyed
Dog destroyed
Example 3: Cache (shared_ptr)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
=== 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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.