콘텐츠로 이동

C++ 표준 스마트 포인터 3종

스마트 포인터는 RAII 원칙을 적용해 동적 할당 메모리를 자동으로 관리합니다. C++11부터 <memory> 헤더에 표준 스마트 포인터 3종이 제공됩니다.

타입소유권참조 카운팅null 가능주 용도
unique_ptr<T>단독 소유없음가능기본 선택, 단일 소유자
shared_ptr<T>공유 소유있음가능여러 소유자 필요 시
weak_ptr<T>비소유 관찰없음항상 가능순환 참조 방지, 캐시

스마트 포인터가 왜 필요한지 이해하기 위해 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 이어야 함
}

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;
}

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;
}
#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;
}

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;
}

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); // 절대 하지 말 것! 이중 해제 발생

#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" 출력 없음 → 누수
}
#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
}
#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 권장 이유

섹션 제목: “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번 할당

#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_ptrmake_unique불가가능가장 먼저 선택, 오버헤드 최소
shared_ptrmake_shared가능가능순환 참조 주의, 카운팅 비용
weak_ptrshared_ptr로부터가능가능사용 전 lock() 필수
// shared_ptr 비용 요소
// 1. 제어 블록 할당 (make_shared 시 1회, 직접 new 시 2회)
// 2. 참조 카운트 원자적 증감 (atomic increment/decrement)
// → 멀티코어에서 캐시 라인 경합 가능
// make_shared 권장 이유 (단일 할당)
auto sp1 = std::make_shared<MyClass>(args...);
// 내부: [MyClass 데이터 | 제어 블록] 연속 메모리 1회 할당
// 직접 new 비권장 (두 번 할당)
std::shared_ptr<MyClass> sp2(new MyClass(args...));
// 내부: MyClass 할당 + 제어 블록 별도 할당
// 주의: make_shared의 단점
// - 커스텀 삭제자 사용 불가 (이 경우 직접 new 사용)
// - weak_ptr이 살아있으면 객체 소멸 후에도 제어 블록(메모리) 유지
// → 큰 객체 + weak_ptr 조합 시 메모리 낭비 가능
// 커스텀 삭제자: unique_ptr
auto file_ptr = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("data.txt", "r"), &fclose);
// 커스텀 삭제자: shared_ptr
auto gpu_buf = std::shared_ptr<void>(
allocate_gpu_buffer(1024),
[](void* p) { free_gpu_buffer(p); });

#include <memory>
#include <iostream>
// 객체 내부에서 자신의 shared_ptr을 안전하게 얻기
class Worker : public std::enable_shared_from_this<Worker> {
public:
void start() {
// this를 shared_ptr로 변환 — 참조 카운트 올바르게 관리
auto self = shared_from_this();
// 비동기 작업에 self를 캡처 → 작업 완료 전 Worker 소멸 방지
std::thread([self]() {
self->do_work();
}).detach();
}
void do_work() {
std::cout << "작업 수행 중\n";
}
};
int main() {
auto w = std::make_shared<Worker>();
w->start();
// ...
}
// 주의: shared_from_this()는 반드시 shared_ptr이 이미 존재할 때만 호출 가능
// Worker w_raw; w_raw.shared_from_this(); // std::bad_weak_ptr 예외!

  • 기본: unique_ptr 사용
  • 공유가 필요한 경우에만: shared_ptr
  • 소유 없이 참조만 필요하면: weak_ptr 또는 raw 포인터 (관찰자)
  • shared_ptr 상호 참조 발견 시 즉시 weak_ptr로 교체
  • 클래스 내부에서 shared_ptr<this> 필요 시 enable_shared_from_this 상속