C++ 표준 스마트 포인터 3종
개요 — 스마트 포인터 비교
Section titled “개요 — 스마트 포인터 비교”스마트 포인터는 RAII 원칙을 적용해 동적 할당 메모리를 자동으로 관리합니다. C++11부터 <memory> 헤더에 표준 스마트 포인터 3종이 제공됩니다.
| 타입 | 소유권 | 참조 카운팅 | null 가능 | 주 용도 |
|---|---|---|---|---|
unique_ptr<T> | 단독 소유 | 없음 | 가능 | 기본 선택, 단일 소유자 |
shared_ptr<T> | 공유 소유 | 있음 | 가능 | 여러 소유자 필요 시 |
weak_ptr<T> | 비소유 관찰 | 없음 | 항상 가능 | 순환 참조 방지, 캐시 |
1. raw 포인터의 문제점
Section titled “1. raw 포인터의 문제점”스마트 포인터가 왜 필요한지 이해하기 위해 raw 포인터의 문제점을 먼저 살펴봅니다.
#include <iostream>#include <stdexcept>
// 문제 1: 예외 발생 시 메모리 누수void Problem1(){ int* p = new int(42); ThrowingFunction(); // 예외 발생 시 아래 delete에 도달하지 못함 delete p; // 누수}
// 문제 2: 이중 해제 (Undefined Behavior)void Problem2(){ int* p = new int(10); delete p; delete p; // 크래시 또는 Undefined Behavior}
// 문제 3: 소유권 불명확 — 누가 delete 해야 하는가?int* CreateValue(){ return new int(100); // 호출자가 delete 해야 함? 문서 없이는 불명확}
// 문제 4: delete vs delete[] 혼용void Problem4(){ int* arr = new int[10]; delete arr; // 잘못됨: delete[] arr 이어야 함}2. unique_ptr — 단독 소유권
Section titled “2. unique_ptr — 단독 소유권”2.1 기본 사용법
Section titled “2.1 기본 사용법”unique_ptr은 하나의 객체를 단 하나의 포인터만 소유합니다. 스코프가 종료되거나 명시적으로 해제할 때 자동으로 delete됩니다.
#include <iostream>#include <memory>#include <string>
struct Texture{ std::string Name; int Width; int Height;
Texture(const std::string& name, int w, int h) : Name(name), Width(w), Height(h) { std::cout << "Texture loaded: " << Name << "\n"; }
~Texture() { std::cout << "Texture released: " << Name << "\n"; }};
int main(){ // make_unique 사용 권장 (new 직접 사용보다 안전하고 효율적) auto tex = std::make_unique<Texture>("diffuse.png", 1024, 1024);
std::cout << tex->Name << " " << tex->Width << "x" << tex->Height << "\n";
// 원시 포인터 접근 Texture* raw = tex.get(); // 소유권 이전 없이 원시 포인터만 얻음 std::cout << raw->Name << "\n";
// 소유권 명시적 해제 tex.reset(); // Texture released: diffuse.png // tex.get() == nullptr
// 스코프 종료 시 자동 해제 (reset 하지 않았다면) auto tex2 = std::make_unique<Texture>("normal.png", 512, 512); // 함수 종료 시 자동으로 "Texture released: normal.png" 출력
return 0;}2.2 소유권 이전 (Move)
Section titled “2.2 소유권 이전 (Move)”unique_ptr은 복사가 불가능하고, std::move로만 소유권을 이전할 수 있습니다.
#include <memory>#include <iostream>
std::unique_ptr<int> CreateValue(){ return std::make_unique<int>(42); // 함수 리턴: 이동 발생}
void ConsumeValue(std::unique_ptr<int> val){ std::cout << "Consumed: " << *val << "\n"; // val 소멸 → 자동 delete}
int main(){ auto p1 = std::make_unique<int>(10); // auto p2 = p1; // 컴파일 에러: 복사 불가
auto p2 = std::move(p1); // 소유권 이전 // p1은 이제 nullptr std::cout << "p1 is null: " << (p1 == nullptr) << "\n"; // 1 std::cout << "p2: " << *p2 << "\n"; // 10
auto p3 = CreateValue(); // 반환값 이동 ConsumeValue(std::move(p3)); // 소유권 함수로 이전
return 0;}2.3 배열 지원
Section titled “2.3 배열 지원”#include <memory>#include <iostream>
int main(){ // unique_ptr<T[]>: delete[] 자동 호출 auto arr = std::make_unique<int[]>(10); for (int i = 0; i < 10; ++i) { arr[i] = i * i; }
for (int i = 0; i < 10; ++i) { std::cout << arr[i] << " "; } std::cout << "\n"; // 스코프 종료 시 delete[] 자동 호출
return 0;}3. shared_ptr — 공유 소유권
Section titled “3. shared_ptr — 공유 소유권”3.1 참조 카운팅 메커니즘
Section titled “3.1 참조 카운팅 메커니즘”shared_ptr은 참조 카운트(use_count)를 유지합니다. 카운트가 0이 되는 시점에 자동으로 메모리를 해제합니다.
#include <memory>#include <iostream>
struct Asset{ std::string Name; explicit Asset(const std::string& name) : Name(name) { std::cout << "Asset created: " << Name << "\n"; } ~Asset() { std::cout << "Asset destroyed: " << Name << "\n"; }};
int main(){ std::shared_ptr<Asset> sp1 = std::make_shared<Asset>("Mesh"); std::cout << "use_count: " << sp1.use_count() << "\n"; // 1
{ std::shared_ptr<Asset> sp2 = sp1; // 복사 → 참조 카운트 증가 std::cout << "use_count: " << sp1.use_count() << "\n"; // 2 std::cout << "sp2->Name: " << sp2->Name << "\n";
std::shared_ptr<Asset> sp3 = sp1; std::cout << "use_count: " << sp1.use_count() << "\n"; // 3 } // sp2, sp3 소멸 → 참조 카운트 감소 std::cout << "use_count after block: " << sp1.use_count() << "\n"; // 1
// sp1 소멸 → 카운트 0 → Asset destroyed: Mesh return 0;}3.2 shared_ptr 제어 블록
Section titled “3.2 shared_ptr 제어 블록”make_shared를 사용하면 객체와 제어 블록을 한 번에 할당해 성능이 좋습니다.
// 권장: make_shared — 한 번의 메모리 할당auto sp1 = std::make_shared<int>(42);
// 비권장: new 직접 사용 — 두 번의 메모리 할당 (객체 + 제어 블록 별도)std::shared_ptr<int> sp2(new int(42));
// 주의: 같은 raw 포인터로 두 개의 shared_ptr 생성 → 이중 해제 위험int* raw = new int(100);std::shared_ptr<int> sp3(raw);// std::shared_ptr<int> sp4(raw); // 절대 하지 말 것! 이중 해제 발생4. weak_ptr — 순환 참조 해결
Section titled “4. weak_ptr — 순환 참조 해결”4.1 순환 참조 문제
Section titled “4.1 순환 참조 문제”#include <memory>#include <iostream>
struct Node{ int Data; std::shared_ptr<Node> Next; // 강한 참조
explicit Node(int data) : Data(data) { std::cout << "Node created: " << Data << "\n"; } ~Node() { std::cout << "Node destroyed: " << Data << "\n"; }};
int main(){ auto a = std::make_shared<Node>(1); auto b = std::make_shared<Node>(2);
a->Next = b; // a → b 참조 b->Next = a; // b → a 참조 (순환!)
// main 종료 시: // a.use_count() = 2 (a 자신 + b->Next) // b.use_count() = 2 (b 자신 + a->Next) // 카운트가 0이 되지 않아 소멸자 호출 안됨 → 메모리 누수! std::cout << "use_count a: " << a.use_count() << "\n"; // 2 std::cout << "use_count b: " << b.use_count() << "\n"; // 2
return 0; // "Node destroyed" 출력 없음 → 누수}4.2 weak_ptr로 순환 참조 해결
Section titled “4.2 weak_ptr로 순환 참조 해결”#include <memory>#include <iostream>
struct Node{ int Data; std::weak_ptr<Node> Next; // 약한 참조로 변경 → 순환 참조 없음
explicit Node(int data) : Data(data) { std::cout << "Node created: " << Data << "\n"; } ~Node() { std::cout << "Node destroyed: " << Data << "\n"; }};
int main(){ auto a = std::make_shared<Node>(1); auto b = std::make_shared<Node>(2);
a->Next = b; // weak_ptr: 참조 카운트 증가 없음 b->Next = a; // weak_ptr: 참조 카운트 증가 없음
// a.use_count() = 1, b.use_count() = 1 // 순환 참조 없음 → main 종료 시 정상 소멸
// weak_ptr 사용: lock()으로 shared_ptr 임시 획득 if (auto nextNode = a->Next.lock()) { std::cout << "a->Next.Data: " << nextNode->Data << "\n"; // 2 }
return 0; // Node destroyed: 2 // Node destroyed: 1}4.3 weak_ptr 사용 패턴
Section titled “4.3 weak_ptr 사용 패턴”#include <memory>#include <iostream>
class EventSystem;
class Listener{public: explicit Listener(const std::string& name) : m_Name(name) {}
void OnEvent(const std::string& eventName) { std::cout << m_Name << " received: " << eventName << "\n"; }
private: std::string m_Name;};
class EventSystem{public: // weak_ptr로 등록: Listener가 소멸되면 자동으로 무효화 void Subscribe(std::weak_ptr<Listener> listener) { m_Listeners.push_back(listener); }
void Broadcast(const std::string& eventName) { // 만료된 weak_ptr 자동 처리 m_Listeners.erase( std::remove_if(m_Listeners.begin(), m_Listeners.end(), [](const std::weak_ptr<Listener>& wp) { return wp.expired(); }), m_Listeners.end() );
for (auto& wp : m_Listeners) { if (auto sp = wp.lock()) { sp->OnEvent(eventName); } } }
private: std::vector<std::weak_ptr<Listener>> m_Listeners;};
int main(){ EventSystem events;
auto listener1 = std::make_shared<Listener>("Player"); auto listener2 = std::make_shared<Listener>("UI");
events.Subscribe(listener1); events.Subscribe(listener2);
events.Broadcast("GameStart");
listener2.reset(); // UI 리스너 소멸
events.Broadcast("LevelUp"); // Player만 수신
return 0;}5. make_unique / make_shared 권장 이유
Section titled “5. make_unique / make_shared 권장 이유”#include <memory>
// 함수 인자 평가 순서가 정해지지 않음 → 예외 발생 시 누수 위험// (C++17에서는 일부 완화됐지만 여전히 권장하지 않음)void Process(std::shared_ptr<int> a, std::shared_ptr<int> b);
// 위험할 수 있는 패턴:// Process(std::shared_ptr<int>(new int(1)),// std::shared_ptr<int>(new int(2)));// new int(1) 성공, new int(2) 예외 → 첫 번째 메모리 누수 가능
// 권장: make_shared / make_unique 사용Process(std::make_shared<int>(1), std::make_shared<int>(2));// 각 make_shared는 원자적으로 처리
// make_shared의 추가 이점: 단일 메모리 할당// shared_ptr<T>(new T) : 객체 + 제어 블록 = 2번 할당// make_shared<T> : 객체 + 제어 블록 = 1번 할당6. 실전 소유권 설계 패턴
Section titled “6. 실전 소유권 설계 패턴”#include <memory>#include <vector>#include <iostream>
class Projectile{public: explicit Projectile(int id) : m_Id(id) { std::cout << "Projectile " << m_Id << " created\n"; } ~Projectile() { std::cout << "Projectile " << m_Id << " destroyed\n"; }
int GetId() const { return m_Id; }
private: int m_Id;};
class ProjectileManager{public: // 생성: 이 매니저가 유일한 소유자 std::weak_ptr<Projectile> Spawn(int id) { auto proj = std::make_shared<Projectile>(id); m_Active.push_back(proj); return proj; // 소유권 없이 weak 참조만 반환 }
// 특정 id 제거 void Destroy(int id) { m_Active.erase( std::remove_if(m_Active.begin(), m_Active.end(), [id](const std::shared_ptr<Projectile>& p) { return p->GetId() == id; }), m_Active.end() ); }
int Count() const { return static_cast<int>(m_Active.size()); }
private: std::vector<std::shared_ptr<Projectile>> m_Active;};
int main(){ ProjectileManager mgr;
auto ref1 = mgr.Spawn(1); auto ref2 = mgr.Spawn(2); auto ref3 = mgr.Spawn(3);
std::cout << "Active: " << mgr.Count() << "\n"; // 3
// 외부에서는 weak_ptr로만 참조 (소유권 없음) if (auto p = ref2.lock()) { std::cout << "Got projectile: " << p->GetId() << "\n"; }
mgr.Destroy(2); // 매니저에서 제거 → shared_ptr 소멸 → Projectile 2 destroyed std::cout << "Active: " << mgr.Count() << "\n"; // 2
// ref2는 이제 만료됨 std::cout << "ref2 expired: " << ref2.expired() << "\n"; // 1
return 0; // 나머지 1, 3 자동 소멸}| 타입 | 생성 함수 | 복사 | 이동 | 주의사항 |
|---|---|---|---|---|
unique_ptr | make_unique | 불가 | 가능 | 가장 먼저 선택, 오버헤드 최소 |
shared_ptr | make_shared | 가능 | 가능 | 순환 참조 주의, 카운팅 비용 |
weak_ptr | shared_ptr로부터 | 가능 | 가능 | 사용 전 lock() 필수 |
소유권 설계 원칙
Section titled “소유권 설계 원칙”- 기본:
unique_ptr사용 - 공유가 필요한 경우에만:
shared_ptr - 소유 없이 참조만 필요하면:
weak_ptr또는 raw 포인터 (관찰자) shared_ptr상호 참조 발견 시 즉시weak_ptr로 교체