개요
안녕하세요. 이번 글에서는 C++의 스마트 포인터(smart pointer)에 대해 알아보겠습니다. C++에서 new로 할당한 메모리는 delete로 직접 해제해야 하는데, 해제를 빠뜨리거나 중간에 예외가 발생하면 메모리 누수(memory leak)가 생깁니다. 스마트 포인터는 이런 수동 관리의 부담을 줄여 주는, 자신이 소멸될 때 가리키던 메모리를 자동으로 해제해 주는 객체입니다. C++11부터 <memory> 헤더에서 unique_ptr, shared_ptr, weak_ptr 세 가지를 제공합니다.
왜 필요한가 — 일반 포인터의 문제
일반 포인터는 아래처럼 할당한 메모리를 직접 해제해야 합니다. 그런데 delete를 빠뜨리거나, 해제 전에 return/예외로 함수를 빠져나가면 그대로 메모리 누수가 됩니다.
void func()
{
int* p = new int(10);
// ... 중간에 예외가 발생하거나 return 하면
if (someError)
return; // delete를 못 하고 빠져나감 → 누수
delete p; // 매번 잊지 않고 해제해야 함
}
스마트 포인터를 쓰면 스코프를 벗어나는 순간 소멸자가 자동으로 호출되어 메모리가 해제되므로, delete를 직접 부를 필요가 없습니다. 이런 방식을 RAII(Resource Acquisition Is Initialization)라고 합니다.
한눈에 보기
| 종류 | 소유 방식 | 복사 | 주 용도 |
|---|---|---|---|
| unique_ptr | 단독 소유 (하나만) | 불가 (이동만 가능) | 기본 선택지 |
| shared_ptr | 공유 소유 (참조 카운트) | 가능 | 여러 곳에서 함께 소유 |
| weak_ptr | 소유하지 않고 참조만 | 가능 | shared_ptr 순환 참조 해결 |
unique_ptr — 단독 소유
하나의 객체를 단 하나의 포인터만 소유하도록 하는 스마트 포인터입니다. 소유권이 유일해야 하므로 복사는 불가능하고, std::move로 소유권을 넘기는 이동만 가능합니다. 생성은 std::make_unique(C++14)를 사용하는 것이 안전합니다.
#include <iostream>
#include <memory>
int main()
{
// make_unique로 생성 (권장)
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::cout << *p1 << "\n"; // 10
// std::unique_ptr<int> p2 = p1; // 컴파일 에러: 복사 불가
std::unique_ptr<int> p2 = std::move(p1); // 소유권 이동
// 이제 p1은 nullptr, p2가 소유
if (!p1)
std::cout << "p1은 비어 있습니다\n";
return 0; // p2가 소멸되며 자동으로 delete
}
shared_ptr — 공유 소유
하나의 객체를 여러 포인터가 함께 소유할 수 있는 스마트 포인터입니다. 내부에 참조 카운트(reference count)를 두어, 복사될 때마다 카운트가 1 증가하고 소멸될 때마다 1 감소합니다. 카운트가 0이 되는 순간 객체가 해제됩니다. 생성은 std::make_shared를 권장합니다.
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::cout << p1.use_count() << "\n"; // 1
{
std::shared_ptr<int> p2 = p1; // 복사 → 함께 소유
std::cout << p1.use_count() << "\n"; // 2
} // p2 소멸 → 카운트 1로 감소
std::cout << p1.use_count() << "\n"; // 1
return 0; // p1 소멸 → 카운트 0 → 자동 해제
}
use_count로 현재 몇 곳에서 소유하고 있는지 확인할 수 있습니다.
weak_ptr — 순환 참조 끊기
shared_ptr끼리 서로를 가리키면 참조 카운트가 영원히 0이 되지 않아, 메모리가 해제되지 않는 순환 참조(circular reference) 문제가 생깁니다.
struct Node
{
std::shared_ptr<Node> next; // 서로 shared_ptr로 참조하면
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; // a가 b를 소유
b->next = a; // b가 a를 소유 → 카운트가 서로 1씩 남아 해제 안 됨
이때 한쪽을 weak_ptr로 바꾸면 됩니다. weak_ptr는 객체를 소유(참조 카운트 증가)하지 않고 그냥 관찰만 하므로, 서로를 가리켜도 카운트가 정상적으로 0이 되어 해제됩니다.
struct Node
{
std::weak_ptr<Node> next; // 한쪽을 weak_ptr로 → 순환 끊김
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 이제 카운트가 서로 올라가지 않으므로 정상 해제됨
한 가지 더, weak_ptr는 소유를 하지 않기 때문에 *로 바로 접근할 수 없습니다. 실제로 쓸 때는 lock()을 호출해 shared_ptr를 얻는데, 대상이 아직 살아 있으면 유효한 shared_ptr를, 이미 해제됐으면 빈 값(nullptr)을 반환합니다. 즉 lock()은 “살아 있는지 확인 + 안전하게 접근”을 한 번에 해 줍니다.
std::weak_ptr<int> w; // 소유하지 않는 관찰자
{
auto sp = std::make_shared<int>(10); // sp가 소유 (카운트 1)
w = sp; // w는 관찰만 (카운트 그대로 1)
if (auto p = w.lock()) // sp가 살아 있음 → 유효한 shared_ptr
std::cout << "살아 있음: " << *p << "\n"; // 여기 실행
} // 스코프 끝 → sp 소멸 → 카운트 0 → 대상 해제
if (auto p = w.lock()) // 대상이 이미 해제됨
std::cout << "살아 있음\n";
else
std::cout << "이미 해제됨\n"; // 이번엔 여기 실행
정리
- unique_ptr — 소유자가 하나뿐일 때. 기본적으로 이것을 먼저 고려합니다.
- shared_ptr — 여러 곳에서 같은 객체를 함께 소유해야 할 때.
- weak_ptr — shared_ptr가 서로를 참조해 생기는 순환 참조를 끊을 때.
스마트 포인터를 쓰면 delete를 직접 호출하지 않아도 스코프를 벗어날 때 자동으로 해제되므로, 메모리 누수와 이중 해제 같은 실수를 크게 줄일 수 있습니다. 이상으로 C++ 스마트 포인터에 대해 간단히 알아보았습니다.