dh_0e

[C++] Smart Pointer 본문

C++

[C++] Smart Pointer

dh_0e 2026. 2. 9. 16:28

Smart Pointer 개요

  • 기존 new, delete를 사용할 때 발생하는 여러 메모리 문제들(memory leak, nullptr issue, Dangling Pointer)을 방지하기 위해 만들어짐
  • Java, C#에서는 Garbage Collector를 사용하여 이 문제를 해결하여 개발자의 생산성을 올려주었지만, 컴퓨터에 부하가 더해져 속도가 느려짐
  • C++ 11 이후 추가되었으며, <memory> 헤더 파일을 선언하여 사용 가능
  • 클래스로 만들어졌으며, 생산자에 포인터를 할당하고, 소멸자에서 이를 delete 해주면서 객체가 만들어질 때 메모리 할당을, 소멸할 때 메모리 해제를 자동으로 해주게끔 만들어 메모리 누수를 방지함
template<typename T>
class smart_pointer{
public:
    smart_pointer(T* p): ptr(p) {}
    ~smart_pointer(){
        delete ptr;
    }
    // 복사 금지: Double Free 방지
    smart_pointer(const smart_pointer&) = delete;
    smart_pointer& operator=(const smart_pointer&) = delete;
    // 연산자 오버로딩
    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }
    // 결과값이 여전히 포인터 타입이라면, 컴파일러는 거기에 다시 ->를 자동으로 붙여서 멤버에 접근
private:
    T* ptr;
}
  • unique_ptr을 class로 직접 구현해 보면 위와 같은 형태로 이루어져 있음
    • 실제 코드는 훨씬 복잡하며 위 코드는 new로 할당한 단일 객체만 관리할 수 있고, new[]로 할당한 배열은 불가능
    • smart_pointer<T[]> 형태에 대한 코드를 추가해야 함
  • delete를 통한 수동 해제가 불가능
  • STL이라 원형은 std:: 생략 필

 

Smart Pointer 종류 및 사용법

1. shared_ptr

  • 여러 포인터가 한 객체를 참조하여 관리할 수 있는 스마트 포인터
  • 해당 값을 참조하고 있는 포인터가 몇 개인지 저장하고 있는 참조 횟수(reference count)가 있음
    • 선언
      • shared_ptr<T> ptr(new T(value)); // 메모리 할당 2번
      • shared_ptr<T> ptr = make_shared<T>(value); // 메모리 할당 1번
      • 속도가 달라서 make_shared를 사용하는 게 좋음

 

  • 참조 횟수(reference count)
    • ptr.use_count(); // main()이 종료되거나 참조 카운트가 0이 되어야 소멸자 작동
#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> ptr=make_shared<int>(10);
    printf("ptr: %d\n",*ptr);
    printf("ref count: %d\n", ptr.use_count());
    
    auto ptr2=ptr;
    (*ptr2)++;
    printf("ptr: %d\n",*ptr);
    printf("ptr2: %d\n",*ptr2);
    printf("ref count: %d\n", ptr2.use_count());
    
    auto ptr3=ptr2;
    (*ptr3)++;
    printf("ptr: %d\n",*ptr);
    printf("ptr2: %d\n",*ptr2);
    printf("ptr3: %d\n",*ptr3);
    printf("ref count: %d\n", ptr3.use_count());

    return 0;
}

 

2. unique_ptr

  • 하나의 포인터만이 객체를 참조하여 관리할 수 있는 스마트 포인터
  • reference count(참조 횟수)가 1로 고정된 shared_ptr
  • 복사 생성자를 지원하지 않으며, move() 함수를 통한 전달만 가능함
  • 선언
    • unique_ptr<T> ptr(new T(value));
    • unique_ptr<T> ptr = make_unique<T>(value);
    • make_unique는 속도 차이는 거의 없지만 가독성과 안정성 때문에 make_unique를 많이 씀
#include <iostream>
#include <memory>
using namespace std;

int main() {
    unique_ptr<int> ptr=make_unique<int>(10);
    printf("ptr: %d\n",*ptr);
    
    auto ptr2=ptr; // error

    return 0;
}

 

3. weak_ptr

  • 약한 참조를 의미하며, shared_ptr를 사용할 때 순환 참조와 같은 문제에 대처하는 데 도움을 줌
    • 순환 참조: 객체 안에 서로의 포인터를 잡고 있어서 서로가 끝나기 전까지 소멸하지 않는 데드락과 유사한 현상을 일으킴

순환 참조 예시

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

class Room; // 전방 선언

class Player {
public:
    std::shared_ptr<Room> currentRoom; // 플레이어가 속한 방
    ~Player() { std::cout << "Player 소멸!\n"; }
};

class Room {
public:
    std::shared_ptr<Player> owner; // 방의 주인인 플레이어
    ~Room() { std::cout << "Room 소멸!\n"; }
};

int main() {
    {
        auto p1 = std::make_shared<Player>();
        auto r1 = std::make_shared<Room>();

        // 서로를 가리킴 (순환 참조 발생)
        p1->currentRoom = r1;
        r1->owner = p1;
    } 
    // 블록을 벗어나도 "소멸!" 메시지가 뜨지 않음 -> 메모리 누수 발생!
    std::cout << "블록 종료\n";
    return 0;
}
  • weak_ptr은 shared_ptr과 유사하지만 가리키는 객체의 수명에 영향을 주지 않는 약한 참조를 하여 순환 참조를 피함
class Room {
public:
    // shared_ptr 대신 weak_ptr 사용!
    std::weak_ptr<Player> owner; 
    ~Room() { std::cout << "Room 소멸!\n"; }
};
  • 위와 같이 owner를 weak_ptr로 바꾸면 r1->owner = p1;을 해도 p1의 참조 횟수가 올라가지 않아 블록이 끝날 때 정상적으로 소멸함

  • weak_ptr로 실제 객체를 쓰기 전에, 객체가 해제됐는지 알 수 없기 때문에 lock() 함수를 호출해서 객체가 아직 살아있는지 확인해야 함