Menu

C++ 스마트 포인터: unique_ptr와 shared_ptr 완벽 정리

스마트 포인터는 힙 메모리를 소유하고 자동으로 해제합니다. unique_ptr, shared_ptr, make_unique, make_shared를 배우고, 왜 이제 new/delete를 거의 쓸 필요가 없는지 알아보세요.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

스마트 포인터가 해결하는 문제

앞 페이지에서는 new로 메모리를 할당하고 delete로 해제했습니다. 이것은 동작하지만 부담을 당신에게 지웁니다. 모든 new에는 대응하는 delete가 필요하며, 이는 코드의 모든 경로에서, 도중에 예외가 던져지는 경로에서도 마찬가지입니다. 하나라도 빠뜨리면 메모리가 누수되고, delete를 두 번 실행하면 힙이 손상됩니다.

스마트 포인터는 힙 메모리의 수명을 스택 위의 일반 객체에 묶어서 이를 해결합니다. 그 객체가 스코프를 벗어나면, 그 소멸자가 당신을 대신해 delete를 실행합니다. 예외가 스택을 풀더라도 보장됩니다. 이 아이디어를 RAII(Resource Acquisition Is Initialization)라고 하며, 스마트 포인터는 <memory> 헤더에 있습니다.

*pp->member를 원시 포인터와 똑같이 사용합니다. 차이는 절대 delete를 호출하지 않는다는 점입니다. 그것은 스마트 포인터가 합니다.

unique_ptr: 단일 소유자, 공유 없음

unique_ptr는 기본으로 손을 뻗어야 할 스마트 포인터입니다. 배타적 소유를 나타냅니다. 어느 시점에서든 정확히 하나의 unique_ptr만 객체를 소유하며, 그 포인터가 소멸되면 객체도 함께 소멸됩니다. 원시 포인터에 비해 런타임 오버헤드가 전혀 없습니다.

make_unique(C++14)로 하나 만드세요. 생성자 인자를 받아 바로 쓸 수 있는 포인터를 돌려줍니다.

소유자는 하나뿐일 수 있으므로, unique_ptr복사할 수 없습니다. 복사하려고 하면 컴파일 오류가 납니다. 그리고 그 오류는, 두 소유자가 같은 객체를 둘 다 delete하려는 상황으로부터 언어가 당신을 보호하는 것입니다.

auto a = make_unique<int>(10);
auto b = a;   // error: call to deleted copy constructor of unique_ptr

소유권을 다른 누군가에게 넘기려면, std::move로 _이동_합니다. 이동 후 원래 포인터는 비어 있게 됩니다(nullptr를 가집니다).

이것이 대부분의 경우 당신이 원하는 모델입니다. 항상 정확히 하나의 명확한 소유자가 있고, 컴파일러가 이를 강제합니다.

shared_ptr: 참조 카운트에 의한 공유 소유

때로는 프로그램의 여러 부분이 정말로 같은 객체를 공유해야 하고, 어느 쪽이 마지막에 끝날지 아무도 모르는 경우가 있습니다. 바로 그것을 위한 것이 shared_ptr입니다. 이것은 _참조 카운트_를 유지합니다. 복사할 때마다 카운트가 올라가고, 소멸할 때마다 내려가며, 카운트가 0이 될 때에만 객체가 해제됩니다.

make_shared로 만드세요.

unique_ptr와 달리 shared_ptr를 복사하는 것은 괜찮습니다. 그것이 바로 핵심입니다. 그 대가는 비용입니다. 참조 카운트는 힙에 저장되고 원자적으로(스레드 안전하게) 갱신되므로, shared_ptrunique_ptr보다 무겁습니다. 누가 무엇을 소유하는지 생각하기를 피하려는 목적이 아니라, 소유가 정말로 공유될 때에만 손을 뻗으세요.

make_sharedshared_ptr<T>(new T(...))보다 더 효율적이기도 합니다. 객체와 제어 블록을 두 번이 아니라 한 번의 할당으로 배치합니다.

weak_ptr와 참조 순환 끊기

shared_ptr에는 고전적인 함정이 하나 있습니다. 두 객체가 서로에게 shared_ptr를 가지고 있으면, 그들의 참조 카운트가 결코 0에 도달하지 않아 둘 다 해제되지 않습니다. 스마트 포인터를 썼는데도 메모리 누수가 생기는 것입니다.

struct Node {
    shared_ptr<Node> next;   // 두 노드가 서로를 가리키면,
};                           // 영원히 서로를 살려 둔다

해결책은 weak_ptr입니다. shared_ptr를 소유하지 않는 관찰자입니다. 참조 카운트를 올리지 않으므로, 객체를 살려 두는 일이 결코 없습니다. 객체를 사용하려면 .lock()을 호출합니다. 객체가 아직 존재하면 shared_ptr를, 이미 사라졌으면 빈 것을 돌려줍니다.

weak_ptr는 "역방향 포인터"와 캐시에 사용하세요. 소유권을 주장하지 않으면서 객체를 참조하고 싶은 모든 곳에서 유용합니다.

흔한 실수와 함정

스마트 포인터는 메모리 버그의 대부분을 없애주지만, 몇 가지 함정은 남아 있습니다.

같은 메모리의 스마트 소유와 원시 소유를 섞지 마세요. 같은 원시 포인터에서 두 개의 스마트 포인터를 만들지 마세요. 각각이 그것을 delete하려고 합니다.

int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw);   // 재앙: 둘 다 같은 int를 삭제한다 (이중 해제)

바로 이것이 make_unique/make_shared를 우선하는 이유입니다. 잘못 사용할 떠도는 원시 포인터가 아예 없습니다.

unique_ptr는 이동만 가능하므로, 소유권을 넘기려면 값으로 전달하세요. 함수가 객체를 _소유_하지 않고 _사용_만 해야 한다면, 대신 일반 참조나 원시 T*를 받으세요. 그저 관찰만 하는 원시 포인터는 전혀 문제없습니다.

void consume(unique_ptr<int> p);   // 소유권을 가져간다 (안으로 이동)
void observe(int* p);              // 보기만 한다, 아무것도 소유하지 않는다

기본으로 shared_ptr에 손을 뻗지 마세요. 자유롭게 복사되니 솔깃하지만, 원자적 참조 카운팅은 실제 성능을 희생시키고, 공유 소유는 수명을 추론하기 어렵게 만듭니다. 기본은 unique_ptr로 하고, 정말로 여러 소유자가 필요할 때에만 shared_ptr로 격상하세요.

배열용 unique_ptr에는 배열 형태가 필요합니다. make_unique<int[]>(n)delete[]를 올바르게 호출하는 unique_ptr<int[]>를 줍니다. 실무에서는 동적 배열std::vector를 우선하세요. 메모리를 대신 관리해 주고, 그 위에 크기 추적까지 제공합니다.

다음: 문자열

이제 메모리 관리를 손에 넣었습니다. 스마트 포인터는 누수 없이 힙 할당을 제공합니다. 당신이 할당하고 여기저기 넘기게 될 가장 흔한 것 중 하나가 텍스트이며, C++는 원시 char* 버퍼보다 훨씬 안전한 도구를 줍니다. 다음 페이지에서는 std::string을 다룹니다. 어떻게 스스로 늘어나는지, 매일 쓰게 될 연산들, 그리고 왜 수동 메모리 작업에서 완전히 벗어나게 해 주는지를 살펴봅니다.

자주 묻는 질문

C++에서 스마트 포인터란 무엇인가요?

스마트 포인터는 <memory>에 있는 객체(unique_ptr, shared_ptr, weak_ptr)로, 원시 포인터를 감싸고 스코프를 벗어날 때 메모리를 자동으로 delete합니다. 수동 delete 없이, 그리고 그것을 잊어버려 생기는 누수 없이 힙 할당을 제공합니다.

unique_ptr와 shared_ptr의 차이는 무엇인가요?

unique_ptr는 자기 객체의 유일한 소유자입니다. 복사할 수 없고 이동만 가능하며, 소멸되는 순간 메모리를 해제합니다. shared_ptr는 참조 카운팅을 통해 공유 소유를 허용합니다. 여러 shared_ptr가 같은 객체를 가리킬 수 있고, 마지막 하나가 소멸될 때에만 객체가 해제됩니다. 정말로 공유 소유가 필요한 경우가 아니라면 unique_ptr를 우선하세요.

모던 C++에서는 make_unique와 new 중 무엇을 써야 하나요?

make_uniquemake_shared를 쓰세요. 이들은 객체를 한 번에 할당하고 감싸므로, 스마트 포인터에 도달하기 전에 결과가 누수될 수 있는 원시 new가 존재하지 않습니다. 경험칙으로, 모던 C++ 코드베이스에는 맨 newdelete가 거의 없어야 합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기