s-nova 님의 블로그
[C++] 스마트 포인터 본문
C/C++에서 메모리를 직접 할당/해제하다 보면 아래와 같은 상황이 발생할 수 있습니다.
Knight* k1 = new Knight();
Knight* k2 = new Knight();
k1->_target = k2;
delete k2;
k1->Attack(); // k2는 이미 해제됐지만 k1은 모름 → 잘못된 메모리 접근!
아직 포인터를 사용하는 중에 다른 곳에서 그 메모리를 먼저 해제해버리는 이 문제를 Use After Free$^{\text{UAF}}$라고 합니다.
이를 해결하기 위해 등장한 것이 스마트 포인터$^{\text{Smart Pointer}}$입니다.
메모리 관리에 RAII$^{\text{Resource Acquisition Is Initialization}}$ 기법을 적용하여,
동적 메모리의 소유권과 수명을 객체로 표현하고 소멸자에서 delete를 자동으로 호출해 메모리 누수를 방지합니다.
1. 레퍼런스 카운팅의 원리
스마트 포인터의 핵심 메커니즘은 레퍼런스 카운팅$^{\text{Reference Counting}}$입니다. 객체를 가리키는 포인터의 수, 즉 소유자 수를 추적하다가 그 수가 0이 되는 순간 메모리를 해제합니다.
내부적으로는 실제 객체 포인터와는 별도로 컨트롤 블록$^{\text{Control Block}}$을 힙에 생성하고, 여기서 참조 횟수를 관리합니다.
| 동작 | refCount 변화 |
| 새 소유자 생성 $($생성자, 복사$)$ | +1 |
| 소유자 소멸 $($소멸자$)$ | -1 |
| refCount가 0이 됨 | 객체 delete 호출 |
2. shared_ptr - 공유 소유권
std::shared_ptr은 여러 포인터가 하나의 객체를 함께 소유할 수 있는 스마트 포인터입니다. 내부 컨트롤 블록의 strong count를 통해 소유자 수를 추적하고, 이 값이 0이 될 때 객체를 해제합니다.
shared_ptr k1(new Knight()); // strong count: 1
{
shared_ptr k2 = k1; // strong count: 2
} // k2 소멸 → strong count: 1
// main 종료 → k1 소멸 → strong count: 0 → Knight delete
💡 팁: 객체 생성 시
new대신std::make_shared<T>()를 사용하면 객체와 컨트롤 블록을 한 번의 힙 할당으로 생성하여 성능이 향상됩니다.
⚠️ 순환 참조 문제$^{\text{Circular Reference}}$
서로가 서로를 shared_ptr로 소유하는 사이클이 생기면, 두 객체 모두 strong count가 영원히 0이 되지 않아 메모리 누수가 발생합니다.
shared_ptr k1(new Knight());
shared_ptr k2(new Knight());
k1->_target = k2; // k2의 strong count: 2
k2->_target = k1; // k1의 strong count: 2
// 스코프 종료 후에도 strong count가 1로 남음 → delete 호출 안 됨!
// → 메모리 누수 발생
이 문제를 해결하기 위해 weak_ptr이 존재합니다.
3. weak_ptr - 소유권 없는 관찰자
std::weak_ptr은 shared_ptr이 소유자$^{\text{owner}}$라면,
객체를 가리키되 소유하지 않는 관찰자$^{\text{observer}}$입니다.
컨트롤 블록에는 두 종류의 카운트가 존재합니다.
| 카운트 | 관리 주체 | 0이 될 때 |
| strong count | shared_ptr |
객체$^{\text{object}}$ 파괴 |
| weak count | weak_ptr |
컨트롤 블록$^{\text{control block}}$ 파괴 |
weak_ptr을 복사하거나 여러 개를 만들어도 strong count에는 영향을 주지 않으므로, 객체의 수명에는 관여하지 않습니다.
대신 해당 객체가 아직 살아있는지 확인하는 정보를 제공합니다.
weak_ptr wk = k1; // strong count 변화 없음, weak count: +1
if (wk.expired() == false)
{
// lock() : 객체가 살아있다면 shared_ptr을 임시 생성 (strong count: +1)
shared_ptr sptr = wk.lock();
sptr->Attack();
}
// sptr 소멸 → strong count: -1
📌 순환 참조 해결: 앞선 Knight 예제에서
_target멤버를shared_ptr대신weak_ptr로 선언하면, 서로를 가리켜도 strong count가 증가하지 않아 순환 참조 문제가 해결됩니다.
4. unique_ptr - 단독 소유권
std::unique_ptr은 하나의 소유자만 허용하는 스마트 포인터입니다.
레퍼런스 카운팅이 없으므로 shared_ptr보다 오버헤드가 작습니다.
복사는 불가능하고 이동$^{\text{move}}$만 가능합니다. 소유권을 다른 unique_ptr로 넘기면, 원래 포인터는 nullptr이 됩니다.
unique_ptr k1 = make_unique();
// unique_ptr k2 = k1; // 컴파일 에러: 복사 불가
unique_ptr k2 = move(k1); // 소유권 이동
// 이후 k1은 nullptr, k2가 유일한 소유자
5. 세 가지 스마트 포인터 비교
| 종류 | 소유자 수 | 복사 | 카운팅 | 주 용도 |
unique_ptr |
1개 | 불가 $($이동만$)$ | 없음 | 단독 소유, 일반적인 동적 할당 |
shared_ptr |
여러 개 | 가능 | strong count | 공유 소유, 수명을 여러 곳에서 관리 |
weak_ptr |
0개 $($관찰만$)$ | 가능 | weak count | 순환 참조 방지, 생존 여부 확인 |
💡 선택 기준: 소유자가 하나라면
unique_ptr을 기본으로 사용하고, 소유권 공유가 필요할 때만shared_ptr을 사용하는 것이 좋습니다. 순환 참조가 발생할 수 있는 구조라면 한쪽을weak_ptr로 대체하세요.
'C++' 카테고리의 다른 글
| [C++] 스레드, CPU 캐시 (0) | 2025.09.18 |
|---|---|
| [C++] lvalue, rvalue, Perfect forwarding (0) | 2025.09.14 |
| [C++] 얕은 복사 vs 깊은 복사 (0) | 2025.08.31 |
| [C++] 다형성, virtual, override (0) | 2025.07.27 |
| [C++] 매크로, constexpr, inline, template (0) | 2025.07.19 |