빠른 비교
#include <memory>
// unique_ptr — 단독 소유, 스코프 종료 시 자동 삭제
auto user = std::make_unique<User>("Mina");
user->print();
// shared_ptr — 참조 카운트 기반 공유 소유
auto a = std::make_shared<Node>(42);
auto b = a; // 참조 카운트 2
// a, b 모두 스코프 종료 시 카운트 0 → 자동 삭제이 카드에서는 "누가 소유하는가"를 먼저 봅니다. 단독 소유면 unique_ptr, 여러 소유자가 정말 필요할 때만 shared_ptr, 관찰만 하면 weak_ptr입니다.
갈리는 기준
먼저 다시 보는 기본형은 아래입니다.
- 자원 수명과 객체 수명 연결: RAII
- 단독 소유:
std::unique_ptr - 공유 소유:
std::shared_ptr - 비소유 관찰:
std::weak_ptr - 생성은
make_unique,make_shared
어떤 소유 모델을 먼저 고르면 되나
| 상황 | 먼저 떠올릴 것 |
|---|---|
| 소유자가 하나뿐 | std::unique_ptr |
| 여러 객체가 진짜로 공동 소유 | std::shared_ptr |
| 소유 없이 수명만 관찰 | std::weak_ptr |
| 스코프 종료 시 자동 해제 | RAII 객체 |
RAII — 자원 수명을 객체 수명에 묶는 원칙
RAII(Resource Acquisition Is Initialization)는 자원을 생성자에서 획득하고 소멸자에서 해제하는 C++ 핵심 패턴입니다. 정상적인 스택 해제 경로에서는 예외가 발생하거나 함수가 일찍 반환해도 소멸자가 호출되므로 정리 경로를 빼먹기 어렵습니다.
// ❌ 수동 관리 — 예외 발생 시 delete 누락 가능
void bad() {
int* p = new int[100];
doSomething(); // 여기서 예외 발생 시 delete[] 호출 안 됨
delete[] p;
}
// ✅ RAII — 예외 여부와 무관하게 소멸자에서 자동 정리
void good() {
auto p = std::make_unique<int[]>(100);
doSomething(); // 예외가 나도 p 소멸자가 delete[] 호출
}unique_ptr — 단독 소유, 비용 없는 스마트 포인터
unique_ptr는 기본 삭제자 기준으로 매우 얇은 래퍼라 추가 오버헤드가 거의 없고, 하나의 소유자만 허용합니다. 복사는 불가능하고 이동만 가능합니다. 소유권 이전이 필요하면 std::move를 사용합니다.
auto p1 = std::make_unique<std::string>("hello");
// 소유권 이전
auto p2 = std::move(p1); // p1은 nullptr이 됨
// p1->size(); // ❌ UB — p1은 더 이상 소유하지 않음
// 함수에 소유권 전달
void consume(std::unique_ptr<std::string> s) { ... }
consume(std::move(p2)); // ✅ 소유권 이전
// 함수 반환 — 이동 자동 적용
std::unique_ptr<Widget> makeWidget() {
return std::make_unique<Widget>(); // ✅ NRVO 또는 이동
}shared_ptr — 참조 카운트 기반 공유 소유
여러 곳에서 동일한 객체를 소유해야 할 때 사용합니다. 내부 참조 카운트가 0이 되면 자동 삭제됩니다. unique_ptr보다 메모리와 시간 비용이 큽니다(제어 블록 별도 할당, 원자적 카운트 증감).
auto s1 = std::make_shared<std::string>("shared");
{
auto s2 = s1; // 참조 카운트 2
auto s3 = s1; // 참조 카운트 3
std::cout << s1.use_count(); // 3
} // s2, s3 소멸 — 카운트 1
// s1 소멸 시 카운트 0 → 자동 삭제// 같은 코드를 단독 소유로 표현할 수 있으면 shared_ptr는 과한 경우가 많다
auto file = std::make_unique<FileHandle>("log.txt");
// 공유가 진짜 필요한 상황이 아니면 unique_ptr가 기본weak_ptr — 순환 참조 방지
shared_ptr끼리 서로를 가리키면 카운트가 0이 되지 않아 누수가 발생합니다. 소유하지 않고 관찰만 해야 할 때 weak_ptr를 사용합니다.
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // ✅ weak_ptr로 역방향 — 순환 참조 방지
};
// weak_ptr 사용 — 잠금 후 유효성 확인
std::weak_ptr<std::string> wp = s1;
if (auto sp = wp.lock()) { // shared_ptr로 잠금
std::cout << *sp; // ✅ 유효한 경우에만 접근
}// ❌ 관찰만 하면 되는데 shared_ptr를 하나 더 들고 있음
struct Observer {
std::shared_ptr<Model> model;
};
// ✅ 소유가 아니라면 weak_ptr로 참조 순환을 끊는다
struct Observer {
std::weak_ptr<Model> model;
};weak_ptr — 캐시 패턴 (옵저버)
weak_ptr는 "대상이 살아있으면 사용, 없으면 건너뜀" 패턴에 유용합니다.
// 캐시 — 원본이 사라지면 자동으로 무효화
class Cache {
std::map<int, std::weak_ptr<Data>> cache_;
public:
std::shared_ptr<Data> get(int id) {
if (auto it = cache_.find(id); it != cache_.end()) {
if (auto sp = it->second.lock()) { // 아직 살아있으면
return sp; // 캐시 히트
}
cache_.erase(it); // 소멸된 항목 정리
}
auto data = std::make_shared<Data>(id);
cache_[id] = data; // weak_ptr로 저장 — 소유권 없음
return data;
}
};
// 옵저버 패턴 — 관찰 대상이 소멸돼도 안전
std::vector<std::weak_ptr<Observer>> observers_;
void notify() {
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[](auto& wp) { return wp.expired(); }), // 소멸된 옵저버 제거
observers_.end());
for (auto& wp : observers_) {
if (auto sp = wp.lock()) sp->onEvent();
}
}선택 기준
- 단독 소유 기본값:
std::unique_ptr - 여러 소유자가 정말 필요할 때:
std::shared_ptr - 소유 없이 관찰만:
std::weak_ptr - 생성은
make_unique/make_shared - 소유권 이전은
std::move
주의할 점
new로 생성한 원시 포인터를 shared_ptr 두 개에 각각 넘기면 이중 해제가 발생합니다. 새로 만드는 경우에는 make_shared/make_unique를 기본값으로 두고, 이미 다른 소유 규칙이 있는 원시 포인터는 제어 블록을 한 번만 만들도록 주의해야 합니다.
Widget* raw = new Widget();
// ❌ 동일한 원시 포인터로 shared_ptr 두 개 생성 — 이중 해제 UB
std::shared_ptr<Widget> p1(raw);
std::shared_ptr<Widget> p2(raw); // raw의 참조 카운트를 모름 → 이중 free
// ✅ make_shared로 한 번에 생성
auto p = std::make_shared<Widget>();
auto q = p; // 동일한 제어 블록 공유 — 안전
// ❌ shared_ptr 남용 — 소유권이 불명확해짐
// unique_ptr로 표현 가능한 경우 shared_ptr 쓰지 않기// ❌ shared_ptr 순환 참조 — 둘 다 영원히 살아남음
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; };
// ✅ 한쪽은 weak_ptr로 끊는다
struct B2 { std::weak_ptr<A> a; };참고 링크
2 sources