본문으로 건너뛰기 C++ 스마트 포인터 | unique_ptr/shared_ptr '메모리 안전' 가이드

C++ 스마트 포인터 | unique_ptr/shared_ptr '메모리 안전' 가이드

C++ 스마트 포인터 | unique_ptr/shared_ptr '메모리 안전' 가이드

이 글의 핵심

스마트 포인터 기본(소유·참조 카운트)에 더해, 제어 블록 레이아웃, make_shared와 직접 생성, weak_ptr lock의 원자성, 커스텀 삭제자 타입 소거, 실무 패턴까지 정리합니다.

🎯 이 글을 읽으면 (읽는 시간: 약 32분)

TL;DR: C++ 메모리 관리의 핵심, 스마트 포인터를 완벽하게 마스터합니다. unique_ptr과 shared_ptr로 메모리 누수와 댕글링 포인터 문제를 해결하는 방법을 배웁니다.

이 글을 읽으면

  • ✅ unique_ptr (독점 소유) vs shared_ptr (공유 소유) 완벽 이해
  • ✅ RAII 원칙과 자동 메모리 관리 마스터
  • ✅ make_unique/make_shared 사용법 및 예외 안전성 습득

실무 활용

  • 🔥 메모리 누수 방지 (자동 해제)
  • 🔥 소유권 명확화 (unique vs shared)
  • 🔥 안전한 리소스 관리 (RAII)

난이도: 고급(심화 포함) | C++11/14 필수 | 실습 코드: 12개+


들어가며

스마트 포인터는 RAII 원칙으로 자동 메모리 관리를 제공하는 포인터 래퍼입니다. raw 포인터의 메모리 누수, 댕글링 포인터 문제를 해결합니다.

C/C++ 예제 코드입니다.

// ❌ raw 포인터 (위험)
int* ptr = new int(10);  // 힙에 메모리 할당
// ... 사용 ...
delete ptr;  // 수동으로 메모리 해제 (깜빡하면 메모리 누수!)
// 문제점:
// 1. delete 깜빡하면 메모리 누수
// 2. 예외 발생 시 delete 실행 안될 수 있음
// 3. 이중 delete 시 크래시
// 4. delete 후 사용 시 UB (댕글링 포인터)

// ✅ 스마트 포인터 (안전)
// std::make_unique: unique_ptr 생성 헬퍼 함수
// RAII 원칙: 객체 생성 시 자원 획득, 소멸 시 자동 해제
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// 스코프 벗어나면 자동으로 delete됨!
// 예외 발생해도 안전하게 메모리 해제

1. unique_ptr - 독점 소유

기본 사용

#include <memory>
#include <iostream>

int main() {
    // 생성
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    
    // 사용
    std::cout << *ptr << std::endl;  // 10
    *ptr = 20;
    std::cout << *ptr << std::endl;  // 20
    
    // nullptr 체크
    if (ptr) {
        std::cout << "유효함" << std::endl;
    }
    
    // 배열
    std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
    arr[0] = 1;
    arr[1] = 2;
    std::cout << arr[0] << ", " << arr[1] << std::endl;  // 1, 2
    
    return 0;
}  // 자동 delete

이동 (복사 불가)

#include <memory>
#include <iostream>

// unique_ptr을 값으로 받음: 소유권 이전
// 함수가 끝나면 자동으로 메모리 해제
void process(std::unique_ptr<int> ptr) {
    std::cout << "값: " << *ptr << std::endl;
}  // ptr 소멸 → 메모리 해제

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    
    // ❌ 복사 불가: unique_ptr은 독점 소유권
    // std::unique_ptr<int> ptr2 = ptr1;  // 컴파일 에러
    // 복사 생성자가 delete되어 있음
    
    // ✅ 이동: std::move로 소유권 이전
    // ptr1의 소유권이 ptr2로 완전히 이동
    // 이동 후 ptr1은 nullptr이 됨 (더 이상 소유하지 않음)
    std::unique_ptr<int> ptr2 = std::move(ptr1);
    
    // ptr1 확인: nullptr인지 체크
    if (!ptr1) {
        std::cout << "ptr1은 nullptr" << std::endl;
    }
    // ptr2 확인: 유효한지 체크
    if (ptr2) {
        std::cout << "ptr2는 유효: " << *ptr2 << std::endl;
    }
    
    // 함수에 전달: 소유권 이전
    // std::move(ptr2): ptr2의 소유권을 process 함수로 이전
    // 함수 호출 후 ptr2는 nullptr
    process(std::move(ptr2));
    
    if (!ptr2) {
        std::cout << "ptr2도 nullptr" << std::endl;
    }
    
    return 0;
}

출력:

ptr1은 nullptr
ptr2는 유효: 10
값: 10
ptr2도 nullptr

2. shared_ptr - 공유 소유

기본 사용

#include <memory>
#include <iostream>

int main() {
    // std::make_shared: shared_ptr 생성 (권장)
    // 제어 블록(참조 카운트)과 객체를 한 번에 할당 (효율적)
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    // use_count(): 현재 참조 카운트 확인
    std::cout << "참조 카운트: " << ptr1.use_count() << std::endl;  // 1
    
    {
        // shared_ptr은 복사 가능: 참조 카운트 증가
        // ptr1과 ptr2는 같은 메모리를 가리킴
        std::shared_ptr<int> ptr2 = ptr1;  // 복사 가능
        // 참조 카운트 2: ptr1, ptr2 두 개가 같은 객체 소유
        std::cout << "참조 카운트: " << ptr1.use_count() << std::endl;  // 2
        std::cout << "ptr1: " << *ptr1 << std::endl;  // 10
        std::cout << "ptr2: " << *ptr2 << std::endl;  // 10
    }  // ptr2 소멸 → 참조 카운트 감소 (2 → 1)
       // 아직 ptr1이 살아있어서 메모리는 해제 안 됨
    
    std::cout << "참조 카운트: " << ptr1.use_count() << std::endl;  // 1
    
    return 0;
}  // ptr1 소멸 → 참조 카운트 0 → 메모리 해제

출력:

터미널에서 다음 명령어를 실행합니다.

참조 카운트: 1
참조 카운트: 2
ptr1: 10
ptr2: 10
참조 카운트: 1

참조 카운팅

#include <memory>
#include <iostream>
#include <vector>

class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " 생성" << std::endl;
    }
    
    ~Resource() {
        std::cout << "Resource " << id_ << " 소멸" << std::endl;
    }
    
    int getId() const { return id_; }
    
private:
    int id_;
};

int main() {
    std::vector<std::shared_ptr<Resource>> resources;
    
    {
        auto r1 = std::make_shared<Resource>(1);
        resources.push_back(r1);
        resources.push_back(r1);
        resources.push_back(r1);
        
        std::cout << "참조 카운트: " << r1.use_count() << std::endl;  // 4
    }  // r1 소멸해도 resources에 남아있음
    
    std::cout << "벡터 크기: " << resources.size() << std::endl;  // 3
    std::cout << "참조 카운트: " << resources[0].use_count() << std::endl;  // 3
    
    resources.clear();  // 모든 참조 제거, Resource 소멸
    
    return 0;
}

출력:

터미널에서 다음 명령어를 실행합니다.

Resource 1 생성
참조 카운트: 4
벡터 크기: 3
참조 카운트: 3
Resource 1 소멸

3. weak_ptr - 순환 참조 방지

순환 참조 문제

#include <memory>
#include <iostream>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A 소멸" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;  // 순환 참조!
    ~B() { std::cout << "B 소멸" << std::endl; }
};

int main() {
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        std::shared_ptr<B> b = std::make_shared<B>();
        
        a->b_ptr = b;
        b->a_ptr = a;  // 순환 참조
        
        std::cout << "a 참조 카운트: " << a.use_count() << std::endl;  // 2
        std::cout << "b 참조 카운트: " << b.use_count() << std::endl;  // 2
    }  // a, b 소멸해도 메모리 해제 안 됨!
    
    std::cout << "블록 종료" << std::endl;
    
    return 0;
}

출력:

a 참조 카운트: 2
b 참조 카운트: 2
블록 종료

문제: A, B 소멸자가 호출되지 않음 (메모리 누수)

weak_ptr로 해결

#include <memory>
#include <iostream>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A 소멸" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // weak_ptr 사용
    ~B() { std::cout << "B 소멸" << std::endl; }
};

int main() {
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        std::shared_ptr<B> b = std::make_shared<B>();
        
        a->b_ptr = b;
        b->a_ptr = a;  // weak_ptr은 참조 카운트 증가 안함
        
        std::cout << "a 참조 카운트: " << a.use_count() << std::endl;  // 1
        std::cout << "b 참조 카운트: " << b.use_count() << std::endl;  // 2
    }  // A, B 모두 정상 소멸
    
    std::cout << "블록 종료" << std::endl;
    
    return 0;
}

출력:

터미널에서 다음 명령어를 실행합니다.

a 참조 카운트: 1
b 참조 카운트: 2
B 소멸
A 소멸
블록 종료

weak_ptr 사용

#include <memory>
#include <iostream>

int main() {
    // weak_ptr: 참조 카운트를 증가시키지 않는 약한 참조
    std::weak_ptr<int> weak;
    
    {
        // shared_ptr 생성
        std::shared_ptr<int> shared = std::make_shared<int>(42);
        // weak_ptr에 할당: 참조 카운트 증가 안함
        // shared의 수명에 영향을 주지 않음
        weak = shared;
        
        // 참조 카운트: 여전히 1 (weak_ptr은 카운트 안함)
        std::cout << "shared 참조 카운트: " << shared.use_count() << std::endl;  // 1
        
        // weak_ptr 사용: lock()으로 shared_ptr 얻기
        // lock(): 객체가 살아있으면 shared_ptr 반환, 아니면 nullptr
        if (auto locked = weak.lock()) {
            // locked: 임시 shared_ptr (참조 카운트 증가)
            std::cout << "값: " << *locked << std::endl;  // 42
            // 참조 카운트 2: shared + locked
            std::cout << "참조 카운트: " << locked.use_count() << std::endl;  // 2
        }  // locked 소멸 → 참조 카운트 1로 복귀
    }  // shared 소멸 → 참조 카운트 0 → 메모리 해제
    
    // weak_ptr 만료 확인
    // expired(): 참조하던 객체가 소멸되었는지 확인
    if (weak.expired()) {
        std::cout << "weak_ptr 만료됨" << std::endl;
    }
    
    // 만료된 weak_ptr에서 lock() 시도
    if (auto locked = weak.lock()) {
        std::cout << "값: " << *locked << std::endl;
    } else {
        // 객체가 이미 소멸되어 lock 실패
        std::cout << "lock 실패" << std::endl;
    }
    
    return 0;
}

출력:

터미널에서 다음 명령어를 실행합니다.

shared 참조 카운트: 1
값: 42
참조 카운트: 2
weak_ptr 만료됨
lock 실패

심화: 제어 블록, make_shared, weak_ptr lock, 삭제자 타입 소거, 실무 패턴

이 절에서는 표준 문서만으로는 드러나지 않는 구현 관점의 직관프로덕션에서의 트레이드오프를 다룹니다. 문장마다 구현체마다 미세한 차이가 있을 수 있으나, libstdc++/libc++/MSVC 계열에서 공통적으로 성립하는 모델을 기준으로 설명합니다.

제어 블록(control block) 레이아웃과 오버헤드

std::shared_ptr가 관리하는 힙 객체는, 사용자가 할당한 관리 대상 객체(managed object) 와, 메타데이터를 담는 제어 블록으로 나뉩니다. 제어 블록에는 일반적으로 강한 참조 카운트(strong count)·약한 참조 카운트(weak count)·삭제자(deleter)·할당자(allocator) 등이 저장됩니다. weak_ptr는 강한 카운트를 올리지 않지만 제어 블록은 공유하므로, 마지막 shared_ptr가 사라져도 weak_ptr가 남아 있으면 제어 블록은 유지됩니다.

sizeof(std::shared_ptr)는 구현에 따라 다르지만, 흔히 원시 포인터 두 개 분량(관리 객체 포인터 + 제어 블록 포인터)입니다. 반면 std::unique_ptr는 삭제자가 상태 없는 기본 타입이면 포인터 한 개에 가깝습니다. 따라서 “스마트 포인터를 값으로 복사·이동하는 빈도”가 높은 경로에서는 shared_ptr 복사가 원자적 연산과 캐시 일관성 비용을 동반한다는 점을 염두에 두어야 합니다.

할당 관점에서 보면, std::shared_ptr<T>(new T(...)) 형태는 객체용 메모리제어 블록용 메모리를 각각 할당하는 경우가 많아, 최소 두 번의 힙 할당이 발생할 수 있습니다. 반대로 std::make_shared는 한 번의 연속 구역에 객체와 제어 블록을 함께 올리는 경우가 많아 할당 횟수 감소캐시 지역성 이점이 있습니다. 다만 마지막 shared_ptr가 사라진 뒤에도 weak_ptr만 남는 상황에서는, make_shared로 묶인 배치 때문에 제어 블록이 차지하는 영역이 객체 본문과 함께 예약되어 있어, 구현에 따라 “객체는 이미 파괴되었는데도 메모리가 덜 반환되는 것처럼 보이는” 현상을 관측할 수 있습니다. 이는 버그가 아니라 수명·할당 전략의 트레이드오프입니다.

std::make_sharedshared_ptr 직접 생성·allocate_shared

std::make_shared는 (1) 예외 안전성: newshared_ptr 생성 사이에 예외가 끼어들며 생길 수 있는 누수 가능성을 줄이며, (2) 할당 횟수·지역성 측면에서 유리한 경우가 많습니다. 팀 코딩 규약에서 “가능하면 make_shared/make_unique”가 기본이 되는 이유입니다.

반면 std::shared_ptr<T>(new T(...), deleter) 나 커스텀 할당자가 필요한 경우에는 직접 생성이 자연스럽습니다. 예를 들어 delete가 아닌 fclose·CloseHandle·COM Release비표준 해제 루틴을 반드시 거처리해야 할 때는 삭제자를 넘기는 shared_ptr 패턴이 흔합니다. 할당 전략까지 제어하려면 std::allocate_shared를 사용해 메모리 풀·전역 할당자와 연동할 수 있습니다.

요약하면, 기본값은 make_shared, 삭제자·할당자가 요구사항을 지배하면 shared_ptr 생성자 또는 allocate_shared로 가져가면 됩니다.

weak_ptr::lock()의 원자성과 TOCTOU

weak_ptr는 강한 참조 카운트를 올리지 않으므로, 관리 객체가 소멸하는 타이밍과 관찰자 코드 사이에 레이스 컨디션이 생길 수 있습니다. lock()은 “객체가 아직 살아 있으면 강한 참조를 하나 만들고 shared_ptr를 실행하여주으며, 이미 파괴되었으면 빈 shared_ptr를 실행하여준다”는 의미에 가깝으며, 구현에서는 제어 블록의 강한 카운트를 원자적으로 증가시키는 경로로 연결됩니다.

실무에서 위험한 패턴은 if (!w.expired()) { auto sp = w.lock(); }처럼 검사와 사용을 나누는 것입니다. expired()가 거짓이었더라도 그 직후 다른 스레드가 마지막 shared_ptr를 버리면 객체는 사라질 수 있어, TOCTOU(time-of-check-time-of-use) 문제가 됩니다. 안전한 형태는 다음과 같습니다.

#include <memory>
#include <iostream>

void safe_use(std::weak_ptr<int> w) {
    if (std::shared_ptr<int> sp = w.lock()) {
        std::cout << *sp << '\n';
    } else {
        std::cout << "이미 소멸됨\n";
    }
}

항상 if (auto sp = weak.lock()) { ... } 한 문장으로 강한 참조를 확보한 뒤에만 객체를 읽거나 멤버를 호출해야 합니다.

커스텀 삭제자와 타입 소거(type erasure)

std::shared_ptr의 삭제자는 타입 시스템상 shared_ptr<T>T에만 노출되고, 실제 삭제 로직은 제어 블록 안에 타입 소거된 형태로 저장됩니다. 그 결과 런타임에 올바른 해제 함수를 호출할 수 있지만, 같은 T라도 삭제자·할당 방식이 다른 shared_ptr끼리는 서로 대입·비교가 제한되는 등(구현·버전에 따름) 미묘한 제약이 생길 수 있습니다.

반면 std::unique_ptr<T, Deleter>에서 Deleter템플릿 인자이므로, 상태를 캡처한 람다 삭제자는 unique_ptr 객체 자체의 크기에 반영될 수 있습니다. 빈 삭제자는 빈 기저 클래스 최적화(EBO)로 비용이 줄어들 수 있지만, 캡처가 있으면 그만큼 무거워집니다. “삭제자가 객체 크기에 들어가도 되는지”는 unique_ptr를 멤버로 둘 때 레이아웃에 직접 영향을 줍니다.

C 스타일 핸들을 예로 들면 다음과 같습니다.

#include <cstdio>
#include <memory>

std::shared_ptr<FILE> open_file(const char* path) {
    return std::shared_ptr<FILE>(std::fopen(path, "rb"), [](FILE* f) {
        if (f) std::fclose(f);
    });
}

shared_ptr는 삭제자가 제어 블록에 들어가므로, 위와 같이 관리 포인터 타입(FILE*)과 실제 리소스 의미를 분리해 표현하기 쉽습니다.

프로덕션에서의 스마트 포인터 패턴

  1. 소유권의 기본값은 unique_ptr: 팩토리·컨테이너가 반환하는 “단일 소유” 리소스는 unique_ptr로 두며, 정말로 여러 경로가 같은 객체 수명을 공유할 때만 shared_ptr로 승격합니다. 불필요한 shared_ptr 남발은 원자적 참조 카운트 갱신 비용을 곳곳에 퍼뜨립니다.
  2. shared_ptr는 공유가 명확할 때만: 캐시, 비동기 콜백 큐, DAG/그래프 노드 등 수명이 본질적으로 공유되는 경우에 한정하는 것이 좋습니다.
  3. weak_ptr는 순환 참조뿐 아니라 옵저버·캐시에도 사용됩니다. lock()에 실패하면 객체가 이미 사라진 것으로 처리하면 됩니다.
  4. std::enable_shared_from_this: 객체가 자기 자신을 shared_ptr로 넘겨야 할 때(예: 비동기 작업에 shared_from_this()). 전제는 이미 shared_ptr로 관리되는 객체 안에서만 호출해야 하며, 스택에만 있는 임시 객체에는 사용할 수 없습니다.
  5. 핫 루프에서 shared_ptr 복사 줄이기: 반복문 안에서 매번 shared_ptr를 복사하기보다, 필요한 최소 범위에서만 지역 shared_ptr를 잡거나, 불변 참조는 const T&/T*로 전달합니다.
  6. Pimpl 관용구: std::unique_ptr<Impl>로 구현 세부를 숨기고 헤더 의존성을 줄입니다. 이동만 정의하면 되는 전형적인 패턴입니다.
#include <memory>
#include <future>

struct Worker : std::enable_shared_from_this<Worker> {
    std::future<void> post_async() {
        return std::async([self = shared_from_this()] {
            (void)self; // 실제로는 멤버 사용
        });
    }
};

// 반드시 shared_ptr로 감싼 뒤에만 shared_from_this() 사용
inline void run() {
    auto w = std::make_shared<Worker>();
    auto f = w->post_async();
    f.wait();
}

4. 실전 예제

예제 1: 리소스 관리

#include <memory>
#include <iostream>
#include <fstream>

class FileHandler {
private:
    std::unique_ptr<std::ofstream> file;
    std::string filename;
    
public:
    FileHandler(const std::string& filename) : filename(filename) {
        file = std::make_unique<std::ofstream>(filename);
        if (!file->is_open()) {
            throw std::runtime_error("파일 열기 실패: " + filename);
        }
        std::cout << "파일 열림: " << filename << std::endl;
    }
    
    ~FileHandler() {
        if (file && file->is_open()) {
            file->close();
            std::cout << "파일 닫힘: " << filename << std::endl;
        }
    }
    
    void write(const std::string& data) {
        if (file && file->is_open()) {
            *file << data << std::endl;
        }
    }
};

int main() {
    try {
        FileHandler handler("output.txt");
        handler.write("Hello");
        handler.write("World");
        // 예외 발생해도 자동으로 파일 닫힘
    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << std::endl;
    }
    
    return 0;
}

예제 2: 팩토리 패턴

#include <memory>
#include <iostream>
#include <string>

class Animal {
public:
    virtual void speak() = 0;
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "멍멍!" << std::endl;
    }
    ~Dog() {
        std::cout << "Dog 소멸" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "야옹!" << std::endl;
    }
    ~Cat() {
        std::cout << "Cat 소멸" << std::endl;
    }
};

std::unique_ptr<Animal> createAnimal(const std::string& type) {
    if (type == "dog") {
        return std::make_unique<Dog>();
    } else if (type == "cat") {
        return std::make_unique<Cat>();
    }
    return nullptr;
}

int main() {
    auto animal1 = createAnimal("dog");
    if (animal1) {
        animal1->speak();
    }
    
    auto animal2 = createAnimal("cat");
    if (animal2) {
        animal2->speak();
    }
    
    auto animal3 = createAnimal("bird");
    if (!animal3) {
        std::cout << "알 수 없는 동물" << std::endl;
    }
    
    return 0;
}

출력:

터미널에서 다음 명령어를 실행합니다.

멍멍!
야옹!
알 수 없는 동물
Cat 소멸
Dog 소멸

예제 3: 캐시 시스템 (shared_ptr)

#include <memory>
#include <unordered_map>
#include <iostream>
#include <string>

class Resource {
private:
    std::string name;
    
public:
    Resource(std::string n) : name(n) {
        std::cout << "리소스 로드: " << name << std::endl;
    }
    
    ~Resource() {
        std::cout << "리소스 언로드: " << name << std::endl;
    }
    
    void use() {
        std::cout << name << " 사용 중" << std::endl;
    }
    
    std::string getName() const { return name; }
};

class ResourceCache {
private:
    std::unordered_map<std::string, std::shared_ptr<Resource>> cache;
    
public:
    std::shared_ptr<Resource> getResource(const std::string& name) {
        if (cache.find(name) == cache.end()) {
            cache[name] = std::make_shared<Resource>(name);
        }
        return cache[name];
    }
    
    void printCacheSize() {
        std::cout << "캐시 크기: " << cache.size() << std::endl;
    }
    
    void clear() {
        cache.clear();
        std::cout << "캐시 비움" << std::endl;
    }
};

int main() {
    ResourceCache cache;
    
    {
        auto r1 = cache.getResource("texture1");
        auto r2 = cache.getResource("texture1");  // 같은 객체
        r1->use();
        
        std::cout << "r1 참조 카운트: " << r1.use_count() << std::endl;  // 3 (r1, r2, cache)
        std::cout << "r2 참조 카운트: " << r2.use_count() << std::endl;  // 3
    }  // r1, r2 소멸해도 캐시에 남아있음
    
    cache.printCacheSize();  // 1
    
    cache.clear();  // 캐시 비우면 리소스 언로드
    
    return 0;
}

출력:

터미널에서 다음 명령어를 실행합니다.

리소스 로드: texture1
texture1 사용 중
r1 참조 카운트: 3
r2 참조 카운트: 3
캐시 크기: 1
캐시 비움
리소스 언로드: texture1

5. 자주 발생하는 문제

문제 1: make_unique/make_shared를 안 쓰는 경우

#include <memory>

void func(std::unique_ptr<int> p1, std::unique_ptr<int> p2) {
    // ...
}

int main() {
    // ❌ 위험한 코드: 예외 안전성 문제
    // func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
    // 평가 순서가 보장 안 되어 누수 가능:
    // 1. new int(1)
    // 2. new int(2)
    // 3. unique_ptr 생성 (예외 발생 시 1, 2 누수)
    
    // ✅ 안전한 코드
    func(std::make_unique<int>(1), std::make_unique<int>(2));
    
    // ✅ 또는
    auto p1 = std::make_unique<int>(1);
    auto p2 = std::make_unique<int>(2);
    func(std::move(p1), std::move(p2));
    
    return 0;
}

문제 2: shared_ptr 순환 참조

#include <memory>
#include <iostream>

// ❌ 순환 참조
class Node {
public:
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // 순환!
    int value;
    
    Node(int v) : value(v) {
        std::cout << "Node " << value << " 생성" << std::endl;
    }
    
    ~Node() {
        std::cout << "Node " << value << " 소멸" << std::endl;
    }
};

void testCircular() {
    auto n1 = std::make_shared<Node>(1);
    auto n2 = std::make_shared<Node>(2);
    
    n1->next = n2;
    n2->prev = n1;  // 순환 참조! 소멸 안 됨
    
    std::cout << "n1 참조 카운트: " << n1.use_count() << std::endl;  // 2
    std::cout << "n2 참조 카운트: " << n2.use_count() << std::endl;  // 2
}

// ✅ weak_ptr 사용
class NodeFixed {
public:
    std::shared_ptr<NodeFixed> next;
    std::weak_ptr<NodeFixed> prev;  // weak_ptr
    int value;
    
    NodeFixed(int v) : value(v) {
        std::cout << "NodeFixed " << value << " 생성" << std::endl;
    }
    
    ~NodeFixed() {
        std::cout << "NodeFixed " << value << " 소멸" << std::endl;
    }
};

void testFixed() {
    auto n1 = std::make_shared<NodeFixed>(1);
    auto n2 = std::make_shared<NodeFixed>(2);
    
    n1->next = n2;
    n2->prev = n1;  // weak_ptr은 참조 카운트 증가 안함
    
    std::cout << "n1 참조 카운트: " << n1.use_count() << std::endl;  // 1
    std::cout << "n2 참조 카운트: " << n2.use_count() << std::endl;  // 2
}

int main() {
    std::cout << "=== 순환 참조 테스트 ===" << std::endl;
    testCircular();
    std::cout << "함수 종료 (소멸자 호출 안 됨!)" << std::endl;
    
    std::cout << "\n=== weak_ptr 테스트 ===" << std::endl;
    testFixed();
    std::cout << "함수 종료 (소멸자 호출됨)" << std::endl;
    
    return 0;
}

출력:

터미널에서 다음 명령어를 실행합니다.

=== 순환 참조 테스트 ===
Node 1 생성
Node 2 생성
n1 참조 카운트: 2
n2 참조 카운트: 2
함수 종료 (소멸자 호출 안 됨!)

=== weak_ptr 테스트 ===
NodeFixed 1 생성
NodeFixed 2 생성
n1 참조 카운트: 1
n2 참조 카운트: 2
NodeFixed 2 소멸
NodeFixed 1 소멸
함수 종료 (소멸자 호출됨)

문제 3: unique_ptr을 함수에 전달

#include <memory>
#include <iostream>

// 방법 1: 소유권 이전
void takeOwnership(std::unique_ptr<int> ptr) {
    std::cout << "소유권 이전: " << *ptr << std::endl;
}

// 방법 2: 참조로 전달 (소유권 유지)
void borrow(const std::unique_ptr<int>& ptr) {
    std::cout << "참조: " << *ptr << std::endl;
}

// 방법 3: raw 포인터로 전달 (소유권 없음)
void observe(int* ptr) {
    if (ptr) {
        std::cout << "관찰: " << *ptr << std::endl;
    }
}

int main() {
    auto ptr = std::make_unique<int>(10);
    
    // ❌ 컴파일 에러
    // takeOwnership(ptr);  // 복사 불가
    
    // ✅ 소유권 이전
    // takeOwnership(std::move(ptr));  // ptr은 nullptr이 됨
    
    // ✅ 참조로 전달
    borrow(ptr);  // ptr 유지
    
    // ✅ raw 포인터로 전달
    observe(ptr.get());  // ptr 유지
    
    std::cout << "ptr 유효: " << (ptr ? "yes" : "no") << std::endl;
    
    return 0;
}

출력:

참조: 10
관찰: 10
ptr 유효: yes

6. 실전 예제: 리소스 관리자

#include <memory>
#include <vector>
#include <iostream>
#include <string>

class ResourceManager {
private:
    std::vector<std::unique_ptr<std::string>> resources_;
    
public:
    // 리소스 추가
    void add(std::unique_ptr<std::string> resource) {
        resources_.push_back(std::move(resource));
    }
    
    // 리소스 생성 및 추가
    void create(const std::string& value) {
        resources_.push_back(std::make_unique<std::string>(value));
    }
    
    // 리소스 가져오기 (소유권 이전)
    std::unique_ptr<std::string> take(size_t index) {
        if (index >= resources_.size()) return nullptr;
        
        auto resource = std::move(resources_[index]);
        resources_.erase(resources_.begin() + index);
        return resource;
    }
    
    // 리소스 개수
    size_t count() const {
        return resources_.size();
    }
    
    // 리소스 출력
    void print() const {
        std::cout << "Resources (" << resources_.size() << "):" << std::endl;
        for (size_t i = 0; i < resources_.size(); ++i) {
            if (resources_[i]) {
                std::cout << "  [" << i << "]: " << *resources_[i] << std::endl;
            } else {
                std::cout << "  [" << i << "]: (moved)" << std::endl;
            }
        }
    }
};

int main() {
    ResourceManager mgr;
    
    // 리소스 추가
    mgr.add(std::make_unique<std::string>("Resource 1"));
    mgr.create("Resource 2");
    mgr.create("Resource 3");
    
    std::cout << "초기 상태:" << std::endl;
    mgr.print();
    
    // 리소스 가져오기
    auto r = mgr.take(1);
    std::cout << "\n가져온 리소스: " << *r << std::endl;
    
    std::cout << "\n남은 리소스:" << std::endl;
    mgr.print();
    
    return 0;
}

출력:

터미널에서 다음 명령어를 실행합니다.

초기 상태:
Resources (3):
  [0]: Resource 1
  [1]: Resource 2
  [2]: Resource 3

가져온 리소스: Resource 2

남은 리소스:
Resources (2):
  [0]: Resource 1
  [1]: Resource 3

정리

핵심 요약

  1. unique_ptr: 독점 소유, 복사 불가, 이동 가능
  2. shared_ptr: 공유 소유, 참조 카운팅·제어 블록(강/약 카운트, 삭제자 등)
  3. weak_ptr: 순환 참조 방지, lock()원자적으로 강한 참조 확보(TOCTOU 방지)
  4. make_unique/make_shared: 예외 안전성·할당·지역성 이점(allocate_shared·직접 생성은 삭제자/할당 요구 시)
  5. 커스텀 삭제자: shared_ptr는 제어 블록에 타입 소거·unique_ptr는 삭제자가 타입에 포함
  6. RAII: 자동 메모리 관리

스마트 포인터 비교

특징unique_ptrshared_ptrweak_ptr
소유권독점공유없음
복사불가가능가능
이동가능가능가능
오버헤드거의 없음(기본 삭제자)제어 블록·원자적 강 카운트제어 블록 참조(약 카운트)
용도기본 선택공유 필요순환 참조 방지
배열지원제한적-

실전 팁

선택 가이드:

  • 기본: unique_ptr
  • 공유 필요: shared_ptr
  • 순환 참조: weak_ptr
  • 배열: unique_ptr<T[]> 또는 vector

성능:

  • unique_ptr: raw 포인터와 동일
  • shared_ptr: 참조 카운팅 오버헤드 (약간)
  • make_shared: 메모리 할당 1번 (최적화)

주의사항:

  • make_unique/make_shared 사용
  • 순환 참조 주의
  • move 후 사용 금지
  • 댕글링 포인터 방지

다음 단계


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. unique_ptr·shared_ptr·weak_ptr 심화: 제어 블록·오버헤드, make_shared 비교, weak_ptr lock, 커스텀 삭제자 타입 소거, 프로덕션 패턴. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, 스마트포인터, unique_ptr, shared_ptr, weak_ptr, 메모리관리 등으로 검색하시면 이 글이 도움이 됩니다.