[C++] 스마트 포인터 (Smart Pointers)

chooha·2026년 1월 5일

C++

목록 보기
16/23

📚 C++ 스마트 포인터 (Smart Pointers)

🔸 스마트 포인터가 필요한 이유

Raw Pointer의 문제점

class Cat
{
public:
    Cat() : mAge{0}
    {
        std::cout << "Cat constructor" << std::endl;
    }
    ~Cat()
    {
        std::cout << "Cat destructor" << std::endl;
    }
private:
    int mAge;
};

void foo(Cat* ptr)
{
    Cat* fooPtr = ptr;
    delete fooPtr;  // ptr이 가리키는 객체 해제
}

int main()
{
    Cat* catPtr = new Cat();
    Cat* catPtr1 = catPtr;  // 같은 객체를 가리킴
    
    foo(catPtr);   // 객체 해제 (1번)
    delete catPtr; // ❌ 이미 해제된 객체를 다시 해제! (Double Delete)
    // Undefined Behavior 발생!
    
    return 0;
}

발생 가능한 문제들
1. Memory Leak: delete를 잊어버림
2. Double Delete: 같은 메모리를 두 번 해제
3. Dangling Pointer: 해제된 메모리를 가리키는 포인터
4. 소유권 불명확: 누가 메모리를 해제해야 하는지 모호함

🔸 RAII (Resource Acquisition Is Initialization)

RAII란?

  • 자원의 획득은 초기화와 함께 이루어져야 한다는 원칙
  • 객체의 생성자에서 자원을 획득하고, 소멸자에서 자동으로 해제
  • C++의 핵심 디자인 패턴

RAII를 통한 자동 메모리 관리

{
    std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
    // Cat 객체 생성 및 소유권 설정
    
    // catPtr 사용...
    
}  // ✅ 스코프를 벗어나면 자동으로 delete 호출!
   // 명시적 delete 불필요

스마트 포인터의 장점

  • 자동 메모리 관리 (RAII)
  • 소유권 명확화
  • 예외 안전성 보장
  • Double Delete 방지

🔸 unique_ptr (Exclusive Ownership)

unique_ptr이란?

  • 독점적 소유권을 가지는 스마트 포인터
  • 복사 불가능, 이동만 가능
  • 가장 가볍고 효율적인 스마트 포인터 (오버헤드 거의 없음)

기본 사용법

#include <memory>

int main()
{
    // ✅ 생성 방법 (C++14 이상)
    std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
    
    // C++11 방식
    std::unique_ptr<Cat> catPtr2{new Cat()};
    
    // 사용
    catPtr->someMethod();
    (*catPtr).someMethod();
    
    // ✅ 자동으로 delete 호출됨
    return 0;
}

복사 불가능, 이동 가능

std::unique_ptr<Cat> catPtr1 = std::make_unique<Cat>();

// ❌ 복사 불가능
std::unique_ptr<Cat> catPtr2 = catPtr1;  // 컴파일 에러!

// ✅ 이동 가능 (소유권 이전)
std::unique_ptr<Cat> catPtr3 = std::move(catPtr1);
// catPtr1은 이제 nullptr
// catPtr3이 객체의 소유권을 가짐

함수 인자로 전달

// ❌ 값으로 전달 (소유권 이전, 원본은 nullptr)
void foo(std::unique_ptr<Cat> ptr)
{
    // ptr이 소유권을 가짐
}  // 여기서 객체 자동 해제

// ✅ 참조로 전달 (소유권 유지, 일시적 사용)
void bar(const std::unique_ptr<Cat>& ptr)
{
    // ptr 사용
}  // 소유권은 호출자가 계속 유지

// ✅ Raw 포인터로 전달 (소유권과 무관하게 사용)
void baz(Cat* ptr)
{
    // ptr 사용
}

int main()
{
    auto catPtr = std::make_unique<Cat>();
    
    // foo(catPtr);  // ❌ 복사 불가
    foo(std::move(catPtr));  // ✅ 소유권 이전, catPtr은 nullptr
    
    auto catPtr2 = std::make_unique<Cat>();
    bar(catPtr2);  // ✅ 소유권 유지
    baz(catPtr2.get());  // ✅ Raw 포인터 전달
    
    return 0;
}

🔸 unique_ptr을 멤버 변수로 사용

클래스 멤버로 사용의 장점

class Zoo
{
public:
    Zoo() : mCat{std::make_unique<Cat>()} {}
    
    // ✅ 소멸자에서 자동으로 delete 호출됨
    ~Zoo() = default;  // 명시적 delete 불필요!
    
private:
    std::unique_ptr<Cat> mCat;  // 자동 메모리 관리
};

주의사항: Copy 불가능

class Zoo
{
public:
    Zoo() : mCat{std::make_unique<Cat>()} {}
    
private:
    std::unique_ptr<Cat> mCat;
};

int main()
{
    Zoo zoo1;
    Zoo zoo2 = zoo1;  // ❌ 컴파일 에러!
    // unique_ptr은 복사 불가능하므로
    // default copy constructor가 delete됨
    
    return 0;
}

해결 방법 1: Copy Constructor/Assignment Delete

class Zoo
{
public:
    Zoo() : mCat{std::make_unique<Cat>()} {}
    
    // ✅ 명시적으로 복사 금지
    Zoo(const Zoo&) = delete;
    Zoo& operator=(const Zoo&) = delete;
    
    // ✅ 이동은 허용
    Zoo(Zoo&&) = default;
    Zoo& operator=(Zoo&&) = default;
    
private:
    std::unique_ptr<Cat> mCat;
};

해결 방법 2: Deep Copy 구현

class Zoo
{
public:
    Zoo() : mCat{std::make_unique<Cat>()} {}
    
    // Deep copy constructor
    Zoo(const Zoo& other)
        : mCat{other.mCat ? std::make_unique<Cat>(*other.mCat) : nullptr}
    {
    }
    
    // Deep copy assignment
    Zoo& operator=(const Zoo& other)
    {
        if (this != &other)
        {
            mCat = other.mCat ? std::make_unique<Cat>(*other.mCat) : nullptr;
        }
        return *this;
    }
    
private:
    std::unique_ptr<Cat> mCat;
};

unique_ptr 사용 가이드라인

  • 기본 선택: 포인터가 필요하면 먼저 unique_ptr 고려
  • 소유권 명확: 한 객체가 자원을 독점적으로 소유
  • 성능: Raw 포인터와 동일한 성능 (오버헤드 없음)
  • 배열 지원: std::unique_ptr<int[]> (하지만 std::vector 권장)

🔸 shared_ptr (Shared Ownership)

shared_ptr이란?

  • 공유 소유권을 가지는 스마트 포인터
  • 참조 카운팅(Reference Counting)을 통해 관리
  • 마지막 shared_ptr이 소멸될 때 객체 자동 해제

기본 사용법

#include <memory>

int main()
{
    // ✅ 생성 (C++11 이상)
    std::shared_ptr<Cat> catPtr1 = std::make_shared<Cat>();
    
    {
        // 복사 가능 (참조 카운트 증가)
        std::shared_ptr<Cat> catPtr2 = catPtr1;  // ✅ OK
        std::shared_ptr<Cat> catPtr3 = catPtr1;
        
        // 참조 카운트 확인
        std::cout << catPtr1.use_count() << std::endl;  // 3
        
    }  // catPtr2, catPtr3 소멸 (참조 카운트 감소)
    
    std::cout << catPtr1.use_count() << std::endl;  // 1
    
    return 0;
}  // catPtr1 소멸, 참조 카운트 0 → Cat 객체 자동 해제

참조 카운팅 동작 원리

std::shared_ptr<Cat> ptr1 = std::make_shared<Cat>();  // count: 1
std::shared_ptr<Cat> ptr2 = ptr1;   // count: 2 (복사)
std::shared_ptr<Cat> ptr3 = ptr2;   // count: 3 (복사)

ptr3.reset();  // count: 2 (ptr3이 소유권 포기)
ptr2 = nullptr;  // count: 1
// ptr1만 남음

// ptr1이 소멸되면 count: 0 → 객체 자동 해제

Control Block

shared_ptr 구조:
┌───────────────┐
│ Data Pointer │ → Cat 객체
├───────────────┤
│ Control Block│ → ┌──────────────────┐
└───────────────┘   │ Reference Count │
                   │ Weak Count      │
                   │ Deleter         │
                   └──────────────────┘
  • shared_ptr은 실제 객체 외에 Control Block이라는 추가 메모리 필요
  • Control Block에는 참조 카운트와 관리 정보 저장
  • 메모리 오버헤드: 포인터 8 bytes + Control Block 약 16-24 bytes

🔸 shared_ptr을 멤버 변수로 사용 시 주의사항

문제 상황: 얕은 복사

class Zoo
{
public:
    Zoo() : mCat{std::make_shared<Cat>()} {}
    
    // Default copy constructor 사용
    // Zoo(const Zoo& other) = default;
    
private:
    std::shared_ptr<Cat> mCat;
};

int main()
{
    Zoo zoo1;
    Zoo zoo2 = zoo1;  // ✅ 복사 가능 (shared_ptr은 복사 가능)
    
    // ❌ 문제: zoo1과 zoo2가 같은 Cat 객체를 공유!
    // zoo1에서 Cat을 수정하면 zoo2에도 영향
    
    return 0;
}

해결 방법 1: 주석으로 명시

class Zoo
{
public:
    Zoo() : mCat{std::make_shared<Cat>()} {}
    
    // ⚠️ Warning: This class uses shared_ptr.
    // Copy constructor performs shallow copy.
    // Both instances will share the same Cat object.
    
private:
    std::shared_ptr<Cat> mCat;
};

해결 방법 2: Deep Copy 함수 제공

class Zoo
{
public:
    Zoo() : mCat{std::make_shared<Cat>()} {}
    
    // Shallow copy는 default 사용
    Zoo(const Zoo&) = default;
    Zoo& operator=(const Zoo&) = default;
    
    // ✅ Deep copy를 위한 별도 함수 제공
    Zoo deepCopy() const
    {
        Zoo newZoo;
        newZoo.mCat = std::make_shared<Cat>(*mCat);  // 새로운 Cat 객체 생성
        return newZoo;
    }
    
private:
    std::shared_ptr<Cat> mCat;
};

int main()
{
    Zoo zoo1;
    Zoo zoo2 = zoo1;           // Shallow copy
    Zoo zoo3 = zoo1.deepCopy(); // Deep copy ✅
    
    return 0;
}

해결 방법 3: Deep Copy를 기본으로 구현

class Zoo
{
public:
    Zoo() : mCat{std::make_shared<Cat>()} {}
    
    // Deep copy constructor
    Zoo(const Zoo& other)
        : mCat{other.mCat ? std::make_shared<Cat>(*other.mCat) : nullptr}
    {
    }
    
    // Deep copy assignment
    Zoo& operator=(const Zoo& other)
    {
        if (this != &other)
        {
            mCat = other.mCat ? std::make_shared<Cat>(*other.mCat) : nullptr;
        }
        return *this;
    }
    
private:
    std::shared_ptr<Cat> mCat;
};

🔸 순환 참조 문제 (Circular Reference)

Memory Leak 발생 상황

class Node
{
public:
    std::shared_ptr<Node> next;  // 다음 노드를 가리킴
    
    ~Node()
    {
        std::cout << "Node destroyed" << std::endl;
    }
};

int main()
{
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;  // node1 → node2 (node2 count: 2)
    node2->next = node1;  // node2 → node1 (node1 count: 2)
    
    // ❌ 순환 참조 발생!
    // node1, node2가 스코프를 벗어나도
    // 서로를 참조하고 있어서 count가 0이 되지 않음
    // → Memory Leak!
    
    return 0;
}
// "Node destroyed" 출력되지 않음!

순환 참조 메모리 구조

node1 (count: 2) → next → node2 (count: 2)
  ↑                         ↓
  └────────── next ──────────┘

main 함수 종료 후:
node1 (count: 1) → next → node2 (count: 1)
  ↑                         ↓
  └────────── next ──────────┘

count가 모두 1이므로 해제되지 않음!

다른 언어의 해결 방법: Garbage Collection

// Java, C#, Python 등
// GC가 Mark and Sweep 알고리즘으로 순환 참조 감지 및 해제
// 1. Mark: 루트에서 도달 가능한 객체 표시
// 2. Sweep: 표시되지 않은 객체 해제

C++에는 GC가 없으므로 weak_ptr로 해결해야 함

🔸 weak_ptr (순환 참조 해결)

weak_ptr이란?

  • 참조 카운팅에 영향을 주지 않는 약한 참조
  • 순환 참조 문제 해결
  • 객체를 관찰(observe)만 하고, 소유하지 않음
  • 사용 전에 shared_ptr로 변환 필요

순환 참조 해결

class Node
{
public:
    std::shared_ptr<Node> next;     // 강한 참조
    std::weak_ptr<Node> prev;       // ✅ 약한 참조 (카운트 증가 X)
    
    ~Node()
    {
        std::cout << "Node destroyed" << std::endl;
    }
};

int main()
{
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;    // node1 → node2 (node2 count: 2)
    node2->prev = node1;    // node2 ⇢ node1 (node1 count: 1, weak!)
    
    // ✅ 순환 참조 해결!
    // node1 count: 1, node2 count: 2
    
    return 0;
}
// node1 소멸 → node1 count: 0 → node1 해제
// node2 소멸 → node2 count: 0 → node2 해제
// "Node destroyed" 두 번 출력됨! ✅

weak_ptr 사용 방법

class Observer
{
public:
    void observe(std::shared_ptr<Cat> cat)
    {
        mWeakCat = cat;  // weak_ptr에 저장 (카운트 증가 X)
    }
    
    void check()
    {
        // weak_ptr을 shared_ptr로 변환 (lock)
        if (auto sharedCat = mWeakCat.lock())  // ✅ 객체가 존재하면
        {
            // 안전하게 사용
            sharedCat->meow();
        }
        else
        {
            std::cout << "Cat is gone!" << std::endl;  // ❌ 객체가 이미 해제됨
        }
    }
    
private:
    std::weak_ptr<Cat> mWeakCat;  // 약한 참조
};

int main()
{
    Observer observer;
    
    {
        auto cat = std::make_shared<Cat>();
        observer.observe(cat);
        observer.check();  // ✅ "Cat exists"
        
    }  // cat 소멸
    
    observer.check();  // ❌ "Cat is gone!"
    
    return 0;
}

weak_ptr 주요 메서드

std::weak_ptr<Cat> weakPtr;

// shared_ptr로 변환 (객체가 존재하면 shared_ptr 반환, 아니면 nullptr)
std::shared_ptr<Cat> sharedPtr = weakPtr.lock();

// 객체가 해제되었는지 확인
bool expired = weakPtr.expired();  // true면 객체 해제됨

// 참조 카운트 확인
long count = weakPtr.use_count();  // shared_ptr의 참조 카운트

🔸 스마트 포인터 선택 가이드

언제 어떤 스마트 포인터를 사용할까?

상황추천 포인터
기본적으로unique_ptr
소유권이 명확한 경우unique_ptr
소유권을 공유해야 하는 경우shared_ptr
순환 참조 방지weak_ptr
캐시 구현weak_ptr
Observer 패턴weak_ptr
배열 관리std::vector (스마트 포인터보다 권장)

일반적인 사용 패턴

// ✅ 1순위: unique_ptr (독점 소유)
std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();

// ✅ 2순위: shared_ptr (공유 필요)
std::shared_ptr<Cat> catPtr = std::make_shared<Cat>();

// ✅ 3순위: weak_ptr (순환 참조 방지, 관찰만)
std::weak_ptr<Cat> weakCat = sharedCat;

// ❌ Raw pointer는 최후의 수단
Cat* rawPtr = new Cat();  // 피하기!
delete rawPtr;

성능 비교

// Raw pointer: 8 bytes
Cat* raw;

// unique_ptr: 8 bytes (오버헤드 없음)
std::unique_ptr<Cat> unique;

// shared_ptr: 16 bytes (포인터 8 + Control Block 포인터 8)
//             + Control Block 16-24 bytes
std::shared_ptr<Cat> shared;

// weak_ptr: 16 bytes (shared_ptr과 동일)
std::weak_ptr<Cat> weak;

🔸 핵심 정리

unique_ptr

  • 독점 소유권, 복사 불가, 이동 가능
  • 오버헤드 없음, 가장 효율적
  • 멤버 변수로 사용 시 복사 생성자 주의
  • 기본 선택지

shared_ptr

  • 공유 소유권, 참조 카운팅
  • 복사 가능, 메모리 오버헤드 있음
  • 순환 참조 주의 (Memory Leak 가능)
  • 멤버 변수로 사용 시 얕은 복사 주의

weak_ptr

  • 참조 카운팅에 영향 없음
  • 순환 참조 해결
  • 사용 전 lock()으로 shared_ptr 변환 필요
  • Observer 패턴, 캐시 구현에 유용

Best Practices
1. Raw pointer 대신 스마트 포인터 사용
2. 기본은 unique_ptr, 필요시 shared_ptr
3. 순환 참조는 weak_ptr로 해결
4. make_unique/make_shared 사용 권장
5. 멤버 변수로 사용 시 복사 의미 명확히


<참고 자료>

코드없는 프로그래밍
C++ Smart Pointers
RAII

1개의 댓글

comment-user-thumbnail
2026년 1월 11일

잘 보고 갑니다!

답글 달기