트랙
/
C++
C++
/
학습 과정
/
스마트 포인터
스마

스마트 포인터 에서 C++

1개의 연습 문제

스마트 포인터 소개

스마트 포인터는 자동 메모리 관리를 제공하도록 설계된 현대 C++의 기능이에요. 일반 포인터에서 흔히 볼 수 있는 메모리 누수와 댕글링 포인터를 막는 데 도움을 줘요. 스마트 포인터는 일반 포인터를 감싸는 래퍼처럼 동작하면서, 포인터가 더 이상 필요 없을 때 메모리를 자동으로 해제하는 기능 같은 것들을 더해 줘요.

일반 문법

스마트 포인터는 보통 C++ 표준 라이브러리에서 클래스 템플릿으로 구현돼 있어요. 가장 많이 쓰이는 스마트 포인터는 std::unique_ptr와 std::shared_ptr 두 가지예요.

유니크 포인터

std::unique_ptr는 객체를 독점적으로 소유하는 스마트 포인터예요. 어느 시점이든 std::unique_ptr 객체 하나만 그 자원을 소유해요. 소유하고 있던 std::unique_ptr가 소멸되거나 리셋되면, 객체를 자동으로 파괴하고 메모리를 해제해요.

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

std::make_unique()의 장점

std::unique_ptr를 만들 때는 new로 메모리를 직접 할당하기보다 std::make_unique()를 쓰는 편이 좋아요. std::make_unique()에는 여러 장점이 있어요:

  1. 예외 안전성: std::make_unique()는 예외 안전성을 보장해요. 객체를 만드는 도중에 예외가 던져지면 메모리가 자동으로 해제되어 메모리 누수를 막아 줘요.
  2. 명확성: std::make_unique()를 쓰면 코드가 더 명확하고 간결해져요. 템플릿 인자가 자동으로 추론되므로 할당할 타입을 일일이 명시할 필요가 없어요.
  3. 최적화 기회: 컴파일러가 new로 직접 메모리를 할당할 때보다 std::make_unique()를 더 효과적으로 최적화할 기회를 가질 수 있어요. 그러면 성능이 좋아질 수도 있어요.
  4. 오용 방지: std::unique_ptr를 직접 생성하면 내부 자원을 직접 삭제하는 것도 가능해요. 그런데 std::unique_ptr이 스코프가 끝날 때 그 자원을 삭제하려고 하면 정의되지 않은 동작이 돼요.

공유 포인터

std::shared_ptr는 여러 std::shared_ptr 객체가 같은 자원을 공유해서 소유할 수 있게 해 주는 스마트 포인터예요. 자원을 참조하는 공유 포인터가 몇 개인지 계속 추적하다가, 그 자원을 소유한 마지막 공유 포인터가 스코프를 벗어나거나 리셋될 때에만 메모리를 해제해요.

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

C++17 이하에서는 std::make_shared<T[]>로 배열을 다루는 std::shared_ptr를 쓰는 것이 곧바로 지원되지 않아요. std::make_shared<T[]>로 배열을 할당하는 것은 가능하지만, 그렇게 만든 배열에서 공유 포인터를 직접 만들면 단일 객체와 배열의 메모리 관리 방식 차이 때문에 정의되지 않은 동작이 생길 수 있어요. 대신 배열을 공유 포인터로 효과적으로 관리하려면 std::vector나 사용자 정의 삭제 함수를 쓰는 것을 고려해 봐요. C++17에서 배열 할당과 공유 포인터를 다룰 때는 늘 사용하는 컴파일러와 표준 라이브러리 구현과의 호환성을 확인해요.

std::make_shared()의 장점

std::make_unique()와 마찬가지로 std::make_shared()도 메모리 효율 향상, 예외 안전성, 가독성 같은 이점을 줘요. 제어 블록과 관리 대상 객체를 위한 메모리 할당을 한 번의 작업으로 합쳐서 효율을 높이고 메모리 누수 위험을 줄여 줘요. 게다가 템플릿 인자가 자동으로 추론되니 코드가 단순해지고 가독성이 좋아져요. C++에서 std::shared_ptr 객체를 다룰 때 std::make_shared()를 쓰면 더 깔끔하고 안전하며 효율적인 코드를 쓸 수 있어요.

약한 포인터

std::weak_ptr는 std::shared_ptr와 짝을 이루는 클래스로, 공유 포인터가 관리하는 객체에 대해 소유하지 않는 "약한" 참조를 제공해요. 자원의 수명에 영향을 주지 않으면서 그 자원에 접근할 수 있게 해 줘요. 약한 포인터는 순환 참조를 끊어 메모리 누수를 막아야 하는 상황에서 유용해요.

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

순환 소유와 std::weak_ptr

std::weak_ptr는 std::shared_ptr를 쓸 때 생길 수 있는 순환 소유, 즉 순환 참조 문제를 해결하려고 만들어졌어요.

순환 소유 상황에서는 둘 이상의 std::shared_ptr 객체가 서로를 참조해서 순환 고리를 만들어요. 그러면 서로에 대한 강한 참조를 쥐고 있어서 어느 객체도 삭제될 수 없고, 결국 메모리 누수로 이어져요.

std::weak_ptr는 공유 객체에 대한 약한 참조를 허용하면서 그 객체의 참조 횟수에는 영향을 주지 않는 방식으로 이 문제를 해결해요. 다시 말해 공유 객체를 관찰하고 접근할 수는 있지만, 그 객체가 삭제되는 것을 막지는 않아요. 공유 객체에 대한 강한 참조가 모두 해제되면 객체는 파괴되고, 그 객체를 관찰하던 약한 포인터는 자동으로 nullptr로 리셋돼요.

다음은 순환 소유를 끊기 위해 std::weak_ptr를 사용하는 짧은 예제예요:

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

"Node constructed"라는 문자열이 두 번 출력되고, 이어서 "Node destructed"라는 문자열이 두 줄 출력돼요.

next가 약한 포인터가 아니라 std::shared_ptr라면 순환 소유 상황이 생겨서 메모리 누수로 이어져요. 두 노드가 서로에 대한 강한 참조를 쥐고 있어서 파괴되지 않기 때문이에요.

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

이 상황에서는 node1과 node2가 std::shared_ptr를 통해 서로에 대한 강한 참조를 쥐고 순환 고리를 만들어요. 프로그램이 끝나면서 reset()으로 강한 참조를 해제하려고 해도, 각 노드가 상대에 대한 강한 참조를 쥐고 있어서 두 노드의 참조 횟수는 0이 되지 않아요. 그래서 노드가 파괴되지 않고 메모리 누수가 생겨요. "Node constructed"라는 문자열은 두 번 출력되지만, "Node destructed"라는 문자열은 한 줄도 출력되지 않아요.

댕글링 포인터와 std::weak_ptr

댕글링 포인터는 이미 삭제된 객체를 포인터가 가리킬 때 생겨요. 이때 포인터를 역참조하면 정의되지 않은 동작이 돼요. std::weak_ptr는 std::shared_ptr이 관리하는 객체에 대해 소유권 없는 약한 참조를 제공해서 댕글링 포인터를 피하도록 도와줘요. std::weak_ptr로 댕글링 포인터를 피하는 방법은 이래요:

  1. std::weak_ptr 생성: std::shared_ptr를 직접 쥐고 있는 대신, 공유 객체에 대한 std::weak_ptr를 만들어요.

  2. 유효성 확인: std::weak_ptr를 쓰기 전에 expired() 함수로 유효한지 확인해요. 이 함수는 연결된 공유 객체가 삭제되었다면 true를 반환해요.

  3. std::weak_ptr 잠금: 공유 객체에 안전하게 접근하려면 lock() 함수를 써요. 이 함수는 객체가 아직 살아 있으면 같은 객체를 가리키는 std::shared_ptr를 반환해요. 객체가 삭제되었다면 lock()은 빈 std::shared_ptr를 반환해요.

다음은 std::weak_ptr로 댕글링 포인터를 피하는 코드 예제예요:

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

이 예제에서 node1->next와 node2->next는 std::weak_ptr 객체예요. 공유 객체에 접근하기 전에 expired() 함수로 유효성을 확인하고, 그다음 lock()으로 공유 객체에 안전하게 접근해요. 이렇게 하면 std::weak_ptr를 다룰 때 댕글링 포인터가 생기는 일을 막을 수 있어요.

사용 권장 사항

기본적으로는 스마트 포인터를 써요. 독점적 소유에는 std::unique_ptr를, 공유 소유에는 std::shared_ptr를 써요. 소유하지 않는 참조나 레거시 코드와 맞물릴 때만 일반 포인터를 남겨 둬요. 대부분의 경우 독점적 소유에는 std::unique_ptr로 충분해요. 참조 횟수 계산 부담 없이 가볍게 메모리를 관리해 주니까요. std::shared_ptr는 꼭 공유 소유가 필요할 때만 아껴 써야 해요. 오버헤드와 복잡함을 끌고 오기 때문이에요. std::weak_ptr는 순환 의존을 끊거나 공유 객체를 관찰하는 특수한 용도로 쓰이고, 흔히 쓰이는 편은 아니에요.

GitHub에서 편집 링크가 새 창이나 탭에서 열려요

스마트 포인터 배우기