Skip to content

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 권장 이유

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번 할당

#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() 필수
  • 기본: unique_ptr 사용
  • 공유가 필요한 경우에만: shared_ptr
  • 소유 없이 참조만 필요하면: weak_ptr 또는 raw 포인터 (관찰자)
  • shared_ptr 상호 참조 발견 시 즉시 weak_ptr로 교체