프로그래밍/C,C++

[Modern C++] smart pointer(unique_ptr, shared_ptr, weak_ptr)

GONII 2025. 3. 10. 11:47

🔹 std::unique_ptr란?

std::unique_ptr는 C++의 스마트 포인터 중 하나로, 단 하나의 포인터만 객체를 소유할 수 있도록 설계된 스마트 포인터입니다.
즉, 같은 객체를 두 개 이상의 unique_ptr가 공유할 수 없으며, 소유권이 한 곳에만 존재합니다.
소유하고 있는 unique_ptr가 범위를 벗어나거나 reset()되면 자동으로 객체가 삭제되어 메모리 누수를 방지할 수 있습니다.

✅ std::unique_ptr의 특징

  1. 단독 소유(Exclusive Ownership)
    • 하나의 unique_ptr만 특정 객체를 소유 가능.
    • 다른 unique_ptr에 복사할 수 없음(copy constructor와 copy assignment가 삭제됨).
  2. 자동 메모리 관리
    • unique_ptr가 스코프를 벗어나면 자동으로 객체를 삭제 (delete 호출 필요 없음).
    • 명시적으로 reset()을 호출하면 객체를 해제하고 새로운 객체를 할당 가능.
  3. 소유권 이동 가능
    • std::move()를 사용하여 소유권을 다른 unique_ptr로 이전 가능.
  4. 성능이 뛰어남
    • 참조 카운팅이 없는 구조라서 shared_ptr보다 오버헤드가 적음.
    • 동적 할당된 리소스를 효율적으로 관리할 수 있음.

📌 std::unique_ptr 사용법

1️⃣ 기본적인 사용법

#include <iostream>
#include <memory>

class Test {
public:
    Test() { std::cout << "Test 생성\n"; }
    ~Test() { std::cout << "Test 소멸\n"; }
    void show() { std::cout << "Test 클래스\n"; }
};

int main() {
    std::unique_ptr<Test> uptr = std::make_unique<Test>(); // 객체 생성 및 관리
    uptr->show(); // 포인터처럼 객체에 접근 가능

    return 0; // uptr이 스코프를 벗어나며 자동으로 Test 객체가 소멸됨
}

출력 예시

Test 생성
Test 클래스
Test 소멸

✅ std::make_unique<Test>()를 사용하면 객체를 생성하면서 unique_ptr를 안전하게 초기화할 수 있음.


2️⃣ 소유권 이전 (std::move)

unique_ptr는 복사할 수 없지만, 소유권을 이전(move)할 수는 있음.

std::unique_ptr<Test> uptr1 = std::make_unique<Test>();
std::unique_ptr<Test> uptr2 = uptr1; // ❌ 오류 (복사 불가능)
std::unique_ptr<Test> uptr3 = std::move(uptr1); // ✅ 이동 가능

🔹 std::move()를 이용해 소유권을 이동하면 uptr1은 더 이상 객체를 소유하지 않음.

#include <iostream>
#include <memory>

class Test {
public:
    Test() { std::cout << "Test 생성\n"; }
    ~Test() { std::cout << "Test 소멸\n"; }
};

int main() {
    std::unique_ptr<Test> uptr1 = std::make_unique<Test>(); // 객체 생성
    std::unique_ptr<Test> uptr2 = std::move(uptr1); // 소유권 이동

    if (!uptr1) {
        std::cout << "uptr1은 이제 nullptr입니다.\n";
    }

    return 0; // uptr2가 스코프를 벗어나며 객체 소멸
}

출력 예시

Test 생성
uptr1은 이제 nullptr입니다.
Test 소멸

✅ uptr1은 더 이상 객체를 소유하지 않고, uptr2가 객체를 소유함.


3️⃣ reset()을 이용한 해제

  • reset()을 호출하면 unique_ptr가 관리하는 객체가 즉시 해제됨.
  • 새 객체를 할당할 수도 있음.
#include <iostream>
#include <memory>

class Test {
public:
    Test() { std::cout << "Test 생성\n"; }
    ~Test() { std::cout << "Test 소멸\n"; }
};

int main() {
    std::unique_ptr<Test> uptr = std::make_unique<Test>();

    uptr.reset(); // 객체 즉시 해제
    std::cout << "reset() 호출 후\n";

    return 0;
}

출력 예시

Test 생성
Test 소멸
reset() 호출 후

✅ reset()을 호출하면 unique_ptr가 즉시 객체를 해제함.


4️⃣ 커스텀 삭제자 사용하기

기본적으로 unique_ptr는 delete를 사용하여 객체를 삭제하지만, 커스텀 삭제자를 지정할 수도 있음.

#include <iostream>
#include <memory>

class Test {
public:
    Test() { std::cout << "Test 생성\n"; }
    ~Test() { std::cout << "Test 소멸\n"; }
};

void customDeleter(Test* ptr) {
    std::cout << "사용자 정의 삭제자 호출\n";
    delete ptr;
}

int main() {
    std::unique_ptr<Test, decltype(&customDeleter)> uptr(new Test(), customDeleter);
    return 0;
}

출력 예시

Test 생성
사용자 정의 삭제자 호출
Test 소멸

✅ customDeleter()가 호출된 후 객체가 삭제됨.


📌 std::unique_ptr vs std::shared_ptr

특징 std::unique_ptr  std::shared_ptr
소유권 단독 소유 여러 shared_ptr가 공유
복사 ❌ 불가능 ✅ 가능 (참조 카운트 증가)
이동(std::move) ✅ 가능 ✅ 가능
성능 shared_ptr보다 가벼움 (참조 카운트 없음) 참조 카운트 증가로 인해 약간의 오버헤드
메모리 관리 자동 해제 참조 카운트가 0이 될 때 해제
순환 참조 문제 없음 발생 가능 (해결하려면 weak_ptr 사용)

🎯 결론

  • std::unique_ptr는 단 하나의 포인터만 객체를 소유할 수 있는 단독 소유 스마트 포인터.
  • shared_ptr보다 가볍고 빠르며, 메모리 누수를 방지할 수 있음.
  • 복사가 불가능하지만, **소유권 이동(std::move)**을 통해 다른 unique_ptr로 넘길 수 있음.
  • reset()으로 객체를 명시적으로 해제할 수도 있음.
  • 동적 할당된 객체를 관리할 때 std::make_unique<T>()를 사용하는 것이 권장됨.

🚀 일반적으로 std::unique_ptr를 기본적으로 사용하고, 객체를 공유해야 할 경우 std::shared_ptr를 사용하면 됨!


🔹 std:: shared_ptr 란?

std::shared_ptr는 C++의 스마트 포인터 중 하나로, 여러 개의 shared_ptr 인스턴스가 같은 객체를 공유할 수 있도록 해주는 스마트 포인터입니다.

✅ std::unique_ptr의 특징

  1. 참조 카운팅(Reference Counting)
    • shared_ptr는 내부적으로 참조 카운트(reference count)를 유지합니다.
    • 하나의 shared_ptr이 새 객체를 소유하면 참조 카운트가 1이 됩니다.
    • shared_ptr가 복사될 때마다 참조 카운트가 증가합니다.
    • shared_ptr가 소멸되거나 reset()이 호출될 때 참조 카운트가 감소합니다.
    • 마지막 shared_ptr이 소멸되면 참조 카운트가 0이 되어 객체가 자동으로 삭제됩니다.
  2. 자동 메모리 관리
    • shared_ptr가 더 이상 필요하지 않으면 자동으로 객체를 해제하므로, 메모리 관리가 쉬워집니다.
  3. 다중 소유 가능
    • 여러 개의 shared_ptr가 같은 객체를 공유할 수 있습니다.

📌 std::unique_ptr 사용법

1️⃣ 기본적인 사용법

#include <iostream>
#include <memory>

class Test {
public:
    Test() { std::cout << "Test 생성\n"; }
    ~Test() { std::cout << "Test 소멸\n"; }
    void show() { std::cout << "Test 클래스\n"; }
};

int main() {
    std::shared_ptr<Test> p1 = std::make_shared<Test>(); // 객체 생성 및 관리 시작
    {
        std::shared_ptr<Test> p2 = p1; // 참조 카운트 증가
        p2->show();
        std::cout << "참조 카운트: " << p1.use_count() << "\n"; // 2
    } // p2가 소멸되며 참조 카운트 감소

    std::cout << "참조 카운트: " << p1.use_count() << "\n"; // 1
} // p1이 소멸되며 객체 삭제

출력 예시

Test 생성
Test 클래스
참조 카운트: 2
참조 카운트: 1
Test 소멸

2️⃣ std::make_shared 사용

  • std::make_shared<T>(...)를 사용하면 shared_ptr를 생성할 때 동적 할당을 한 번만 수행하므로 성능이 더 좋습니다.
std::shared_ptr<Test> p = std::make_shared<Test>();

3️⃣ reset()과 use_count()

  • reset()을 호출하면 현재 객체와의 연결이 끊어지고, 참조 카운트가 감소합니다.
  • use_count()는 현재 객체를 참조하는 shared_ptr의 개수를 반환합니다.
p1.reset(); // 객체 소멸
std::cout << p1.use_count() << "\n"; // 0

📌주의할 점

  1. 순환 참조 문제
    • shared_ptr를 서로가 참조하는 경우, 참조 카운트가 0이 되지 않아 메모리 누수가 발생할 수 있습니다.
    • 이를 방지하려면 std::weak_ptr를 함께 사용해야 합니다.
#include <iostream>
#include <memory>

class B; // 전방 선언

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

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

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; // 순환 참조 발생

    return 0; // 메모리 누수 발생 (A와 B가 소멸되지 않음)
}

해결 방법

  • std::shared_ptr 대신 std::weak_ptr 사용
class B;
class A {
public:
    std::weak_ptr<B> b_ptr; // weak_ptr 사용
    ~A() { std::cout << "A 소멸\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B 소멸\n"; }
};

🎯 결론

  • shared_ptr는 참조 카운트를 사용하여 자동으로 객체를 관리하는 스마트 포인터.
  • std::make_shared를 사용하면 성능이 향상됨.
  • 순환 참조를 피하기 위해 std::weak_ptr를 함께 사용해야 함.

🔹 std::weak_ptr 란?

std::weak_ptr는 std::shared_ptr와 함께 사용되는 스마트 포인터로, 소유권을 가지지 않는 참조를 제공하여 순환 참조(circular reference) 문제를 방지하는 역할을 합니다.

✅ std::weak_ptr의 특징

  1. 객체의 소유권을 가지지 않음
    • shared_ptr처럼 참조 카운트(use_count())를 증가시키지 않음.
    • shared_ptr가 관리하는 객체가 소멸되면 weak_ptr는 자동으로 무효(invalid)가 됨.
  2. 순환 참조(Circular Reference) 문제 해결
    • shared_ptr끼리 서로를 참조하면 참조 카운트가 0이 되지 않아 메모리 누수가 발생할 수 있음.
    • weak_ptr을 사용하면 참조 카운트가 증가하지 않아 객체가 정상적으로 해제됨.
  3. lock()을 사용하여 shared_ptr로 변환 가능
    • lock()을 호출하면 유효한 객체가 있을 경우 shared_ptr을 반환하여 안전하게 객체를 사용 가능.
    • 이미 소멸된 객체라면 nullptr을 반환.

📌 사용법

1️⃣ 기본적인 weak_ptr 사용

#include <iostream>
#include <memory>

class Test {
public:
    Test() { std::cout << "Test 생성\n"; }
    ~Test() { std::cout << "Test 소멸\n"; }
    void show() { std::cout << "Test 클래스\n"; }
};

int main() {
    std::shared_ptr<Test> sp = std::make_shared<Test>(); // 객체 생성
    std::weak_ptr<Test> wp = sp; // weak_ptr에 shared_ptr 할당 (소유권 X)

    std::cout << "use_count: " << sp.use_count() << "\n"; // 1

    if (auto shared = wp.lock()) { // weak_ptr을 shared_ptr로 변환
        shared->show();
        std::cout << "use_count (lock 후): " << sp.use_count() << "\n"; // 2
    }

    sp.reset(); // shared_ptr 해제 (객체 소멸)
    
    if (wp.expired()) {
        std::cout << "객체가 소멸됨\n";
    }

    return 0;
}

출력 예시

Test 생성
use_count: 1
Test 클래스
use_count (lock 후): 2
Test 소멸
객체가 소멸됨

2️⃣ weak_ptr을 활용한 순환 참조 해결

❌ 순환 참조 문제 (메모리 누수 발생)

#include <iostream>
#include <memory>

class B; // 전방 선언

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

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B 소멸\n"; }
};

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; // ❌ 순환 참조 발생 (메모리 누수)

    return 0; // A와 B가 소멸되지 않음 (use_count가 0이 되지 않음)
}

출력 없음 (메모리 누수 발생)


✅ weak_ptr을 사용하여 순환 참조 해결

#include <iostream>
#include <memory>

class B; // 전방 선언

class A {
public:
    std::weak_ptr<B> b_ptr; // weak_ptr 사용 (소유권 X)
    ~A() { std::cout << "A 소멸\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B 소멸\n"; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b; // weak_ptr 사용
    b->a_ptr = a; // shared_ptr 유지

    return 0; // A와 B가 정상적으로 소멸됨
}

출력

B 소멸
A 소멸

✅ weak_ptr을 사용하여 B가 A를 소유하지 않게 되었기 때문에, B가 먼저 소멸되고 A가 정상적으로 소멸됨.


📌 주요 함수

함수 설명

expired() 관리하는 객체가 소멸되었는지 확인 (true: 소멸됨)
lock() shared_ptr을 생성하여 객체 사용 (nullptr 반환 가능)
reset() 현재 weak_ptr을 해제
use_count() shared_ptr의 참조 카운트 반환

🎯 결론

  • std::weak_ptr은 std::shared_ptr과 함께 사용되며 소유권을 가지지 않음.
  • 순환 참조 문제를 해결하는 데 사용됨.
  • expired()와 lock()을 사용하여 객체가 유효한지 확인하고 안전하게 접근 가능.
  • std::make_shared와 함께 사용하면 더 효율적.
반응형

'프로그래밍 > C,C++' 카테고리의 다른 글

[Modern C++] std::thread  (0) 2025.03.11
[Modern C++] 우측값 레퍼런스  (0) 2025.02.24
[Mordern C++] golang-style defer 만들기  (1) 2024.11.29
함수(function)  (0) 2015.03.24
상속의 기능  (0) 2015.02.14