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

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

이 글의 핵심

C++ 스마트 포인터: unique_ptr/shared_ptr 메모리 안전 가이드. unique_ptr - 독점 소유·shared_ptr - 공유 소유.

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

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 포인터의 메모리 누수, 댕글링 포인터 문제를 해결합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 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 - 독점 소유

기본 사용

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

이동 (복사 불가)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

2. shared_ptr - 공유 소유

기본 사용

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 → 메모리 해제

출력: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

참조 카운팅

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

3. weak_ptr - 순환 참조 방지

순환 참조 문제

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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로 해결

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

weak_ptr 사용

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

4. 실전 예제

예제 1: 리소스 관리

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 팩토리 패턴

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

5. 자주 발생하는 문제

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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 순환 참조

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

=== 순환 참조 테스트 ===
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을 함수에 전달

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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. 실전 예제: 리소스 관리자

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

출력: 아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

초기 상태:
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: 순환 참조 방지, 참조 카운트 증가 안함
  4. make_unique/make_shared: 예외 안전성, 성능 이점
  5. 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 후 사용 금지
  • 댕글링 포인터 방지

다음 단계


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

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

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3