Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

s-nova 님의 블로그

[C++] 스마트 포인터 본문

C++

[C++] 스마트 포인터

s-nova 2025. 9. 8. 14:56

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_ptrshared_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