C++ 스마트 포인터
Raw 포인터의 위험에서 벗어나 안전하고 명확한 메모리 관리를 위한 현대 C++의 핵심 도구
들어가며 — 왜 스마트 포인터인가?
C++은 메모리를 직접 제어할 수 있다는 점이 강점이지만, 그만큼 실수하기도 쉽다.
// 흔히 저지르는 실수들
Foo* p = new Foo();
if (error) return; // delete 호출 안 됨 → 메모리 누수
delete p;
p->use(); // use-after-free → 크래시
delete p; // double-free → 크래시
이런 문제를 근본적으로 해결하는 것이 스마트 포인터다.
핵심 아이디어는 RAII(Resource Acquisition Is Initialization) — 객체의 수명이 끝나는 시점에 자동으로 리소스를 해제한다.
<memory> 헤더에 포함되어 있으며, C++11부터 표준 라이브러리에 포함되었다.
1. unique_ptr — 단독 소유
개념
unique_ptr은 하나의 객체에 대해 단 하나의 소유자만 존재함을 보장한다.
소유자(unique_ptr)가 scope를 벗어나면 자동으로 delete가 호출된다.
#include <memory>
{
auto p = std::make_unique<Foo>(42);
p->doSomething();
} // 여기서 자동 delete — 수동 delete 불필요
소유권 이전
복사는 불가능하지만 std::move로 소유권을 이전할 수 있다.
auto p1 = std::make_unique<Foo>();
auto p2 = std::move(p1); // p1 → nullptr, p2가 소유권 획득
// p1은 이제 nullptr
함수 인자로 전달
// 소유권을 넘길 때
void take(std::unique_ptr<Foo> p);
take(std::move(p));
// 단순 사용만 할 때 (소유권 이전 X)
void use(Foo& obj); // nullable이 아닌 경우
void use(Foo* obj); // nullable인 경우
커스텀 Deleter
배열이나 특수 해제 로직이 필요할 때:
auto arr = std::unique_ptr<int[]>(new int[10]);
auto fp = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("file.txt", "r"), fclose
);
언제 쓸까?
- 기본 선택지. 명확한 단일 소유자가 있을 때 항상
unique_ptr을 먼저 고려 - 팩토리 함수 반환값
- 클래스 멤버로 동적 할당 객체를 관리할 때
2. shared_ptr — 공유 소유
개념
여러 곳에서 동일 객체를 공유 소유할 수 있다.
내부적으로 참조 카운트(reference count)를 관리하며, 카운트가 0이 되면 자동 delete.
auto p1 = std::make_shared<Foo>(); // 카운트: 1
{
auto p2 = p1; // 카운트: 2
auto p3 = p1; // 카운트: 3
} // p2, p3 소멸 → 카운트: 1
// p1 소멸 → 카운트: 0 → delete
내부 구조 — Control Block
shared_ptr은 실제 포인터 외에 control block을 별도로 할당한다.
shared_ptr → [ptr] → Foo 객체
↘ [control block]
- strong count (shared_ptr 수)
- weak count (weak_ptr 수)
- deleter
make_shared를 쓰면 객체와 control block을 한 번에 할당하므로 성능상 유리하다.
// Bad: 할당 2번 (Foo + control block 따로)
std::shared_ptr<Foo> p(new Foo());
// Good: 할당 1번
auto p = std::make_shared<Foo>();
스레드 안전성
참조 카운트 증감은 atomic 연산으로 스레드 안전하다.
단, 객체 자체에 대한 접근은 별도 동기화가 필요하다.
// 카운트 관리는 안전
auto p2 = p1; // atomic increment — OK
// 객체 접근은 별도 뮤텍스 필요
std::lock_guard<std::mutex> lock(mtx);
p1->modify(); // 이건 직접 보호해야 함
언제 쓸까?
- 여러 객체나 스레드가 동일 리소스를 공유할 때
- 캐시, Observer 패턴, 그래프 자료구조
- 소유권이 불분명하거나 동적으로 바뀔 때
3. weak_ptr — 약한 참조
개념
shared_ptr이 관리하는 객체를 소유권 없이 참조한다.
참조 카운트를 증가시키지 않아 객체 수명에 영향을 주지 않는다.
auto sp = std::make_shared<Foo>();
std::weak_ptr<Foo> wp = sp; // 카운트 변화 없음 (여전히 1)
if (auto locked = wp.lock()) { // 살아있으면 shared_ptr 반환
locked->use(); // 안전하게 사용
} else {
// 이미 해제된 경우
}
순환 참조 문제 해결
shared_ptr만 쓰면 순환 참조 시 메모리가 영원히 해제되지 않는다.
// 문제: 순환 참조
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // 서로 참조 → 둘 다 카운트 1로 남음 → 누수
};
// 해결: 한쪽을 weak_ptr로
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 소유하지 않으므로 순환 차단
};
Observer 패턴에서의 활용
Subject가 Observer보다 오래 살 때, weak_ptr로 dangling 포인터를 방지:
class Subject {
std::vector<std::weak_ptr<Observer>> observers_;
void notify() {
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[](const auto& wp) { return wp.expired(); }),
observers_.end()
);
for (auto& wp : observers_) {
if (auto obs = wp.lock()) obs->onEvent();
}
}
};
언제 쓸까?
shared_ptr순환 참조를 끊을 때- 객체가 살아있을 수도, 없을 수도 있는 optional 참조
- 캐시에서 실제 소유는 외부에 있고 캐시는 약하게 참조할 때
한눈에 비교
| 항목 | unique_ptr |
shared_ptr |
weak_ptr |
|---|---|---|---|
| 소유권 | 단독 | 공유 | 없음 |
| 복사 | 불가 | 가능 | 가능 |
| 이동 | 가능 | 가능 | 가능 |
| 참조 카운트 | 없음 | strong count | weak count |
| 오버헤드 | 거의 없음 | control block | control block |
| 주 용도 | 기본 소유 | 공유 소유 | 순환 참조 차단 |
실전 가이드라인
1. new 대신 make_unique / make_shared 사용
// Bad
std::unique_ptr<Foo> p(new Foo(42));
std::shared_ptr<Foo> sp(new Foo(42));
// Good
auto p = std::make_unique<Foo>(42);
auto sp = std::make_shared<Foo>(42);
예외 안전성도 보장되고, make_shared는 할당 횟수도 줄어든다.
2. 소유권이 없으면 raw reference/pointer 사용
스마트 포인터를 함수 인자로 무조건 넘기는 것은 과도한 설계다.
void process(std::unique_ptr<Foo> p); // 소유권 이전이 목적일 때만
void process(Foo& obj); // 단순 사용 → raw reference 충분
void process(Foo* obj); // nullable인 경우
3. 선택 기준 플로우
동적 할당이 필요한가?
└─ Yes: 소유자가 하나인가?
└─ Yes: unique_ptr ← 기본 선택
└─ No: shared_ptr
└─ 순환 참조 위험? → weak_ptr로 한쪽 차단
마치며
| 상황 | 선택 |
|---|---|
| 명확한 단일 소유자 | unique_ptr |
| 여러 소유자 필요 | shared_ptr |
| 소유 없이 참조만 | weak_ptr 또는 raw & / * |
| 수명이 프로그램 전체 | static unique_ptr 또는 싱글턴 |
현대 C++에서 raw new / delete를 직접 쓸 일은 거의 없다.
스마트 포인터를 기본으로, 소유권을 코드로 명확히 표현하는 것이 핵심이다.
'언어 > C, C++' 카테고리의 다른 글
| C++ static 과 그 주변 개념들 (0) | 2026.08.31 |
|---|---|
| [C/C++] 공백을 포함한 파일을 문자열 변수에 쓰기 (0) | 2024.05.27 |
| CMake, CMakeLists.txt 정리 (0) | 2024.02.19 |
| make, Makefile 정리 (0) | 2024.02.13 |
| [C++] stringstream (0) | 2024.01.02 |
