C++ unique_ptr와 shared_ptr, 면접에서 고르는 기준
소유자가 하나면 unique_ptr, 여러 곳이 수명을 공유하면 shared_ptr를 고릅니다. 관찰만 하거나 공유 소유 포인터 사이의 순환 참조를 끊어야 하면 weak_ptr를 사용하며, 선택 기준은 객체의 소유권과 수명입니다.
이 글은 cppreference의 unique_ptr·shared_ptr·weak_ptr 문서를 2026-08-27 기준으로 정리한 일반 설명이며, 표준 버전과 구현에 따라 세부 동작은 달라질 수 있습니다.
unique_ptr는 무엇을 보장하나?
한 줄 답: 이 타입은 객체를 단독으로 소유·관리하고, 스코프를 벗어날 때 관리하던 객체를 처분하며, 소유권은 복사가 아니라 이동으로만 넘깁니다.
<memory>에 정의된 C++11 스마트 포인터 unique_ptr는 한 시점에 관리 객체의 소유권을 하나의 포인터에 두는 모델입니다. 이 타입이 스코프를 벗어나면 관리 객체는 기본 삭제자 또는 사용자가 지정한 삭제자를 통해 처분됩니다. 이런 수명 관리는 RAII와 연결되므로 소유자와 정리 시점을 코드 구조로 확인할 수 있습니다.
C++14부터 제공되는 std::make_unique를 사용해 생성할 수 있습니다. 복사 생성과 복사 대입은 삭제되어 있으며, 비-const 객체만 std::move를 통해 다른 단독 소유 포인터로 소유권을 이전합니다. 이동이 끝난 원래 포인터는 비어 있으며, 공식 짧은 흐름은 assert(!p)로 이를 확인합니다.
std::unique_ptr<D> p = std::make_unique<D>();
std::unique_ptr<D> q = pass_through(std::move(p));
assert(!p);
기본 삭제자는 단일 객체에 delete, 배열 특수화에 delete[]를 사용합니다. get()은 관리 객체를 가리키는 원시 포인터를 반환하지만 소유권은 옮기지 않으며, release() 뒤에는 호출자가 반환된 원시 포인터를 직접 처분해야 합니다. pImpl처럼 불완전 타입을 사용할 때는 생성이 가능하더라도 기본 삭제자가 호출되는 소멸·이동 대입·reset 시점에 완전한 타입이어야 합니다.
shared_ptr는 언제 필요한가?
한 줄 답: 여러 shared_ptr가 같은 객체의 수명을 함께 보장해야 할 때 사용하며, 마지막 남은 공유 소유자가 없어질 때 객체가 파괴됩니다.
<memory>에 정의된 C++11 공유 소유 스마트 포인터입니다. 단순히 여러 곳에서 접근하는 것만으로는 조건이 충족되지 않으며, 여러 주체가 객체의 수명을 함께 보장해야 할 때 공유 소유 모델을 적용합니다. 한 소유자가 다른 포인터를 대입하거나 reset()을 호출해도 다른 공유 소유자가 남아 있으면 객체 수명은 계속됩니다.
마지막 공유 소유자가 소멸하거나 다른 포인터를 대입·reset()하면 객체가 파괴되고 메모리가 해제됩니다. 공유는 다른 공유 소유 포인터의 값을 복사 생성 또는 복사 대입해서 만들며, 이미 관리 중인 객체의 원시 포인터로 새 공유 소유 포인터를 구성하면 정의되지 않은 동작입니다. 따라서 공유가 필요할 때도 기존 포인터의 값을 복사하는 경로를 유지해야 합니다.
std::shared_ptr<Base> p = std::make_shared<Derived>();
이 공식 예시는 기반 타입으로 파생 타입 객체를 공유 소유하는 형태입니다. p의 복사본이 남아 있는 동안 원래 포인터가 소유권을 내려놓아도, 마지막 복사본이 사라지는 시점에 파생 객체가 파괴됩니다.
일반적인 구현에서는 저장 포인터와 제어 블록 포인터를 보통 함께 가지며, 제어 블록에는 관리 객체 또는 그 포인터, 삭제자·할당자 정보, 공유 소유자 수와 약한 참조 수 등이 들어갈 수 있습니다. 이는 ‘typical implementation’ 범위의 설명이므로 특정 메모리 크기나 실행 시간을 뜻하지 않습니다. 서로 다른 공유 소유 포인터 객체는 같은 소유권을 공유해도 추가 동기화 없이 멤버 함수를 호출할 수 있지만, 같은 포인터 객체를 여러 스레드가 동시에 접근하면서 비-const 멤버 함수를 사용하면 데이터 레이스가 발생하므로 동기화 또는 std::atomic<shared_ptr>를 검토해야 합니다.
weak_ptr는 왜 같이 보나?
한 줄 답: weak_ptr는 공유 소유를 늘리지 않는 관찰 참조이며, 객체가 아직 살아 있을 때만 lock()으로 임시 공유 소유를 얻고 순환 참조를 끊는 데 사용합니다.
<memory>에 정의된 C++11 비소유 참조입니다. 이 참조만으로는 객체에 접근할 수 없으며, lock()으로 공유 소유 포인터를 얻은 뒤 성공 여부를 확인해야 합니다. expired()는 참조 대상 객체가 이미 삭제되었는지를 관찰합니다.
std::weak_ptr<int> gw;
auto sp = std::make_shared<int>(42);
gw = sp;
if (std::shared_ptr<int> spt = gw.lock())
/* use *spt */;
else
/* gw is expired */;
lock()이 성공하면 반환된 공유 소유 포인터가 접근하는 동안 객체의 수명을 임시로 보장합니다. 원래 공유 소유자가 사라지는 순간에도 이 포인터가 남아 있으면 객체는 그 포인터가 사라질 때까지 살아 있습니다. 반대로 lock() 결과가 비어 있으면 이미 객체가 삭제되어 접근할 수 없습니다.
공유 소유 포인터끼리 서로를 가리키는 순환이 만들어지면 외부 소유자가 사라져도 참조 수가 0이 되지 않아 메모리가 해제되지 않을 수 있습니다. 이 고리의 한쪽을 약한 참조로 두면 해당 연결이 소유권을 늘리지 않으므로 순환을 끊을 수 있습니다. 이미 공유 소유로 관리되는 객체가 같은 소유권을 공유하는 추가 포인터를 만들어야 한다면 enable_shared_from_this와 shared_from_this()를 사용하며, shared_ptr(this)를 새로 만드는 방식은 피해야 합니다. 공유 소유로 관리되지 않는 객체에서 shared_from_this()를 호출하면 C++17 이후 std::bad_weak_ptr를 던집니다.
임베디드에서 고를 때 무엇을 보나?
한 줄 답: 임베디드에서도 먼저 단독·공유·관찰 중 소유권을 정하고, 공유 소유가 필요할 때 제어 블록·할당 방식·약한 참조의 수명 조건을 확인합니다.
판단 순서는 파괴 지점과 수명 책임에서 시작합니다. 파괴를 책임지는 주체가 하나이고 소유권을 다른 곳으로 넘길 필요가 있으면 이동 가능한 단독 소유 모델을 검토하며, 여러 모듈이 객체의 생존을 함께 보장해야 하면 공유 소유 모델을 검토합니다. 수명을 연장하지 않고 상태만 확인하거나 공유 소유의 고리를 끊는 경우에는 약한 참조와 lock()을 검토합니다.
make_shared는 객체와 제어 블록을 통상 한 번의 할당으로 만들지만, 이는 표준이 권고하는 일반적인 구현 설명이며 모든 구현에서의 보장을 뜻하지 않습니다. 원시 포인터로 shared_ptr<T>(new T(...))를 만드는 형태는 객체와 제어 블록에 적어도 두 번의 할당이 필요합니다. 제어 블록과 참조 수 갱신에 관한 비용은 일반적인 구현의 조건으로 확인해야 하며, RAM·실행 시간 수치로 확대하지 않습니다.
다만 make_shared로 만든 제어 블록을 약한 소유자가 계속 참조하면 모든 공유 소유자가 끝난 뒤에도 T가 차지하던 저장 공간이 약한 소유자가 사라질 때까지 남을 수 있습니다. 관리 객체가 큰지와 약한 참조의 수명이 긴지를 함께 확인해야 합니다. make_shared는 사용자 정의 삭제자를 받을 수 없고 선택한 생성자가 public이어야 하므로 이 조건이 필요한 경우 생성 방식도 달라질 수 있습니다.
pImpl처럼 불완전 타입을 다루는 경우에도 두 모델의 조건이 다릅니다. 단독 소유 포인터는 불완전 타입의 원시 포인터로 구성될 수 있지만 기본 삭제자를 사용하는 경우 소멸·이동 대입·reset 시점에 완전한 타입이 필요하며, 공유 소유 포인터의 원시 포인터 생성자와 reset(Y*)에는 완전한 타입이 필요합니다. 배열 특수화는 delete[]를 사용한다는 점을 확인하고, 공유 소유 배열과 make_shared 배열 오버로드는 대상 표준 버전에 따라 확인해야 합니다.
C++ 기술면접 질문 10가지는 이 글과 별개로 C++ 개념을 점검하는 참고 글입니다.
FAQ
한 줄 답: 소유권 이전, 마지막 공유 소유자, 순환 참조, 불완전 타입, 동시 접근의 범위를 구분하면 흔한 선택 오류를 줄일 수 있습니다.
질문은 소유권을 누가 보장하는지와 객체 수명이 언제 끝나는지를 기준으로 나누어야 합니다. 동시 접근은 포인터 객체 자체와 관리 객체의 접근을 구분해서 확인해야 합니다.
| 질문 | 답변 |
|---|---|
| 단독 소유 포인터를 복사해도 됩니까? | 복사 생성과 복사 대입은 삭제되어 있습니다. 비-const 객체에서 std::move를 사용해 소유권을 이전해야 합니다. |
| 공유 소유 객체는 언제 파괴됩니까? | 마지막 공유 소유자가 소멸하거나 다른 포인터를 대입·reset()할 때 객체가 파괴되고 메모리가 해제됩니다. |
| 약한 참조로 객체를 바로 사용할 수 있습니까? | 그렇게 사용할 수 없습니다. lock()으로 공유 소유 포인터를 얻고 성공 여부를 확인한 뒤 접근해야 합니다. |
| 공유 소유 포인터가 서로를 가리키면 어떻게 됩니까? | 외부 소유자가 사라져도 참조 수가 0이 되지 않을 수 있습니다. 고리의 한쪽을 약한 참조로 두어 소유권 순환을 끊어야 합니다. |
| 공유 소유 포인터를 여러 스레드가 사용해도 항상 안전합니까? | 서로 다른 포인터 객체는 같은 소유권을 공유해도 추가 동기화 없이 멤버 함수를 호출할 수 있습니다. 같은 포인터 객체를 비-const 멤버 함수와 함께 동시에 접근하면 데이터 레이스가 발생하므로 동기화 또는 std::atomic<shared_ptr>를 사용해야 합니다. |
| 이미 관리 중인 원시 포인터로 공유 소유 포인터를 새로 만들어도 됩니까? | 그렇게 구성해서는 안 됩니다. 이미 다른 공유 소유 포인터가 관리하는 원시 포인터로 새 공유 소유 포인터를 구성하면 정의되지 않은 동작입니다. |
출처
한 줄 답: 소유권·수명·삭제·제어 블록·약한 참조의 근거는 모두 cppreference의 <memory> 문서에서 확인할 수 있습니다.
본문은 2026-08-27 기준으로 확인한 공식 cppreference 문서의 정의와 일반적인 구현 설명을 바탕으로 정리했습니다. make_shared의 할당과 약한 참조가 남은 경우의 저장 공간, 스레드 동시 접근은 표준·구현 조건과 함께 읽어야 합니다.