danbibibi
article thumbnail
Published 2026. 8. 10. 13:36
C++ 스마트 포인터 언어/C, C++

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
profile

danbibibi

@danbibibi

꿈을 꾸는 시간은 멈춰 있는 것이 아냐 두려워하지 마 멈추지 마 푸른 꿈속으로