C++ 스마트 포인터 | unique_ptr/shared_ptr "메모리 안전" 가이드
이 글의 핵심
C++ 스마트 포인터에 대한 실전 가이드입니다. unique_ptr/shared_ptr 등을 예제와 함께 상세히 설명합니다.
들어가며
스마트 포인터는 RAII 원칙으로 자동 메모리 관리를 제공하는 포인터 래퍼입니다. raw 포인터의 메모리 누수, 댕글링 포인터 문제를 해결합니다.
// ❌ raw 포인터 (위험)
int* ptr = new int(10); // 힙에 메모리 할당
// ... 사용 ...
delete ptr; // 수동으로 메모리 해제 (깜빡하면 메모리 누수!)
// 문제점:
// 1. delete 깜빡하면 메모리 누수
// 2. 예외 발생 시 delete 실행 안될 수 있음
// 3. 이중 delete 시 크래시
// 4. delete 후 사용 시 UB (댕글링 포인터)
// ✅ 스마트 포인터 (안전)
// std::make_unique: unique_ptr 생성 헬퍼 함수
// RAII 원칙: 객체 생성 시 자원 획득, 소멸 시 자동 해제
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// 스코프 벗어나면 자동으로 delete됨!
// 예외 발생해도 안전하게 메모리 해제
1. unique_ptr - 독점 소유
기본 사용
#include <memory>
#include <iostream>
int main() {
// 생성
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// 사용
std::cout << *ptr << std::endl; // 10
*ptr = 20;
std::cout << *ptr << std::endl; // 20
// nullptr 체크
if (ptr) {
std::cout << "유효함" << std::endl;
}
// 배열
std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
arr[0] = 1;
arr[1] = 2;
std::cout << arr[0] << ", " << arr[1] << std::endl; // 1, 2
return 0;
} // 자동 delete
이동 (복사 불가)
#include <memory>
#include <iostream>
// unique_ptr을 값으로 받음: 소유권 이전
// 함수가 끝나면 자동으로 메모리 해제
void process(std::unique_ptr<int> ptr) {
std::cout << "값: " << *ptr << std::endl;
} // ptr 소멸 → 메모리 해제
int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
// ❌ 복사 불가: unique_ptr은 독점 소유권
// std::unique_ptr<int> ptr2 = ptr1; // 컴파일 에러
// 복사 생성자가 delete되어 있음
// ✅ 이동: std::move로 소유권 이전
// ptr1의 소유권이 ptr2로 완전히 이동
// 이동 후 ptr1은 nullptr이 됨 (더 이상 소유하지 않음)
std::unique_ptr<int> ptr2 = std::move(ptr1);
// ptr1 확인: nullptr인지 체크
if (!ptr1) {
std::cout << "ptr1은 nullptr" << std::endl;
}
// ptr2 확인: 유효한지 체크
if (ptr2) {
std::cout << "ptr2는 유효: " << *ptr2 << std::endl;
}
// 함수에 전달: 소유권 이전
// std::move(ptr2): ptr2의 소유권을 process 함수로 이전
// 함수 호출 후 ptr2는 nullptr
process(std::move(ptr2));
if (!ptr2) {
std::cout << "ptr2도 nullptr" << std::endl;
}
return 0;
}
출력:
ptr1은 nullptr
ptr2는 유효: 10
값: 10
ptr2도 nullptr
2. shared_ptr - 공유 소유
기본 사용
#include <memory>
#include <iostream>
int main() {
// std::make_shared: shared_ptr 생성 (권장)
// 제어 블록(참조 카운트)과 객체를 한 번에 할당 (효율적)
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// use_count(): 현재 참조 카운트 확인
std::cout << "참조 카운트: " << ptr1.use_count() << std::endl; // 1
{
// shared_ptr은 복사 가능: 참조 카운트 증가
// ptr1과 ptr2는 같은 메모리를 가리킴
std::shared_ptr<int> ptr2 = ptr1; // 복사 가능
// 참조 카운트 2: ptr1, ptr2 두 개가 같은 객체 소유
std::cout << "참조 카운트: " << ptr1.use_count() << std::endl; // 2
std::cout << "ptr1: " << *ptr1 << std::endl; // 10
std::cout << "ptr2: " << *ptr2 << std::endl; // 10
} // ptr2 소멸 → 참조 카운트 감소 (2 → 1)
// 아직 ptr1이 살아있어서 메모리는 해제 안됨
std::cout << "참조 카운트: " << ptr1.use_count() << std::endl; // 1
return 0;
} // ptr1 소멸 → 참조 카운트 0 → 메모리 해제
출력:
참조 카운트: 1
참조 카운트: 2
ptr1: 10
ptr2: 10
참조 카운트: 1
참조 카운팅
#include <memory>
#include <iostream>
#include <vector>
class Resource {
public:
Resource(int id) : id_(id) {
std::cout << "Resource " << id_ << " 생성" << std::endl;
}
~Resource() {
std::cout << "Resource " << id_ << " 소멸" << std::endl;
}
int getId() const { return id_; }
private:
int id_;
};
int main() {
std::vector<std::shared_ptr<Resource>> resources;
{
auto r1 = std::make_shared<Resource>(1);
resources.push_back(r1);
resources.push_back(r1);
resources.push_back(r1);
std::cout << "참조 카운트: " << r1.use_count() << std::endl; // 4
} // r1 소멸해도 resources에 남아있음
std::cout << "벡터 크기: " << resources.size() << std::endl; // 3
std::cout << "참조 카운트: " << resources[0].use_count() << std::endl; // 3
resources.clear(); // 모든 참조 제거, Resource 소멸
return 0;
}
출력:
Resource 1 생성
참조 카운트: 4
벡터 크기: 3
참조 카운트: 3
Resource 1 소멸
3. weak_ptr - 순환 참조 방지
순환 참조 문제
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 소멸" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a_ptr; // 순환 참조!
~B() { std::cout << "B 소멸" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // 순환 참조
std::cout << "a 참조 카운트: " << a.use_count() << std::endl; // 2
std::cout << "b 참조 카운트: " << b.use_count() << std::endl; // 2
} // a, b 소멸해도 메모리 해제 안됨!
std::cout << "블록 종료" << std::endl;
return 0;
}
출력:
a 참조 카운트: 2
b 참조 카운트: 2
블록 종료
문제: A, B 소멸자가 호출되지 않음 (메모리 누수)
weak_ptr로 해결
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 소멸" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // weak_ptr 사용
~B() { std::cout << "B 소멸" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr은 참조 카운트 증가 안함
std::cout << "a 참조 카운트: " << a.use_count() << std::endl; // 1
std::cout << "b 참조 카운트: " << b.use_count() << std::endl; // 2
} // A, B 모두 정상 소멸
std::cout << "블록 종료" << std::endl;
return 0;
}
출력:
a 참조 카운트: 1
b 참조 카운트: 2
B 소멸
A 소멸
블록 종료
weak_ptr 사용
#include <memory>
#include <iostream>
int main() {
// weak_ptr: 참조 카운트를 증가시키지 않는 약한 참조
std::weak_ptr<int> weak;
{
// shared_ptr 생성
std::shared_ptr<int> shared = std::make_shared<int>(42);
// weak_ptr에 할당: 참조 카운트 증가 안함
// shared의 수명에 영향을 주지 않음
weak = shared;
// 참조 카운트: 여전히 1 (weak_ptr은 카운트 안함)
std::cout << "shared 참조 카운트: " << shared.use_count() << std::endl; // 1
// weak_ptr 사용: lock()으로 shared_ptr 얻기
// lock(): 객체가 살아있으면 shared_ptr 반환, 아니면 nullptr
if (auto locked = weak.lock()) {
// locked: 임시 shared_ptr (참조 카운트 증가)
std::cout << "값: " << *locked << std::endl; // 42
// 참조 카운트 2: shared + locked
std::cout << "참조 카운트: " << locked.use_count() << std::endl; // 2
} // locked 소멸 → 참조 카운트 1로 복귀
} // shared 소멸 → 참조 카운트 0 → 메모리 해제
// weak_ptr 만료 확인
// expired(): 참조하던 객체가 소멸되었는지 확인
if (weak.expired()) {
std::cout << "weak_ptr 만료됨" << std::endl;
}
// 만료된 weak_ptr에서 lock() 시도
if (auto locked = weak.lock()) {
std::cout << "값: " << *locked << std::endl;
} else {
// 객체가 이미 소멸되어 lock 실패
std::cout << "lock 실패" << std::endl;
}
return 0;
}
출력:
shared 참조 카운트: 1
값: 42
참조 카운트: 2
weak_ptr 만료됨
lock 실패
4. 실전 예제
예제 1: 리소스 관리
#include <memory>
#include <iostream>
#include <fstream>
class FileHandler {
private:
std::unique_ptr<std::ofstream> file;
std::string filename;
public:
FileHandler(const std::string& filename) : filename(filename) {
file = std::make_unique<std::ofstream>(filename);
if (!file->is_open()) {
throw std::runtime_error("파일 열기 실패: " + filename);
}
std::cout << "파일 열림: " << filename << std::endl;
}
~FileHandler() {
if (file && file->is_open()) {
file->close();
std::cout << "파일 닫힘: " << filename << std::endl;
}
}
void write(const std::string& data) {
if (file && file->is_open()) {
*file << data << std::endl;
}
}
};
int main() {
try {
FileHandler handler("output.txt");
handler.write("Hello");
handler.write("World");
// 예외 발생해도 자동으로 파일 닫힘
} catch (const std::exception& e) {
std::cerr << "에러: " << e.what() << std::endl;
}
return 0;
}
예제 2: 팩토리 패턴
#include <memory>
#include <iostream>
#include <string>
class Animal {
public:
virtual void speak() = 0;
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "멍멍!" << std::endl;
}
~Dog() {
std::cout << "Dog 소멸" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "야옹!" << std::endl;
}
~Cat() {
std::cout << "Cat 소멸" << std::endl;
}
};
std::unique_ptr<Animal> createAnimal(const std::string& type) {
if (type == "dog") {
return std::make_unique<Dog>();
} else if (type == "cat") {
return std::make_unique<Cat>();
}
return nullptr;
}
int main() {
auto animal1 = createAnimal("dog");
if (animal1) {
animal1->speak();
}
auto animal2 = createAnimal("cat");
if (animal2) {
animal2->speak();
}
auto animal3 = createAnimal("bird");
if (!animal3) {
std::cout << "알 수 없는 동물" << std::endl;
}
return 0;
}
출력:
멍멍!
야옹!
알 수 없는 동물
Cat 소멸
Dog 소멸
예제 3: 캐시 시스템 (shared_ptr)
#include <memory>
#include <unordered_map>
#include <iostream>
#include <string>
class Resource {
private:
std::string name;
public:
Resource(std::string n) : name(n) {
std::cout << "리소스 로드: " << name << std::endl;
}
~Resource() {
std::cout << "리소스 언로드: " << name << std::endl;
}
void use() {
std::cout << name << " 사용 중" << std::endl;
}
std::string getName() const { return name; }
};
class ResourceCache {
private:
std::unordered_map<std::string, std::shared_ptr<Resource>> cache;
public:
std::shared_ptr<Resource> getResource(const std::string& name) {
if (cache.find(name) == cache.end()) {
cache[name] = std::make_shared<Resource>(name);
}
return cache[name];
}
void printCacheSize() {
std::cout << "캐시 크기: " << cache.size() << std::endl;
}
void clear() {
cache.clear();
std::cout << "캐시 비움" << std::endl;
}
};
int main() {
ResourceCache cache;
{
auto r1 = cache.getResource("texture1");
auto r2 = cache.getResource("texture1"); // 같은 객체
r1->use();
std::cout << "r1 참조 카운트: " << r1.use_count() << std::endl; // 3 (r1, r2, cache)
std::cout << "r2 참조 카운트: " << r2.use_count() << std::endl; // 3
} // r1, r2 소멸해도 캐시에 남아있음
cache.printCacheSize(); // 1
cache.clear(); // 캐시 비우면 리소스 언로드
return 0;
}
출력:
리소스 로드: texture1
texture1 사용 중
r1 참조 카운트: 3
r2 참조 카운트: 3
캐시 크기: 1
캐시 비움
리소스 언로드: texture1
5. 자주 발생하는 문제
문제 1: make_unique/make_shared를 안 쓰는 경우
#include <memory>
void func(std::unique_ptr<int> p1, std::unique_ptr<int> p2) {
// ...
}
int main() {
// ❌ 위험한 코드: 예외 안전성 문제
// func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
// 평가 순서가 보장 안되어 누수 가능:
// 1. new int(1)
// 2. new int(2)
// 3. unique_ptr 생성 (예외 발생 시 1, 2 누수)
// ✅ 안전한 코드
func(std::make_unique<int>(1), std::make_unique<int>(2));
// ✅ 또는
auto p1 = std::make_unique<int>(1);
auto p2 = std::make_unique<int>(2);
func(std::move(p1), std::move(p2));
return 0;
}
문제 2: shared_ptr 순환 참조
#include <memory>
#include <iostream>
// ❌ 순환 참조
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // 순환!
int value;
Node(int v) : value(v) {
std::cout << "Node " << value << " 생성" << std::endl;
}
~Node() {
std::cout << "Node " << value << " 소멸" << std::endl;
}
};
void testCircular() {
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // 순환 참조! 소멸 안됨
std::cout << "n1 참조 카운트: " << n1.use_count() << std::endl; // 2
std::cout << "n2 참조 카운트: " << n2.use_count() << std::endl; // 2
}
// ✅ weak_ptr 사용
class NodeFixed {
public:
std::shared_ptr<NodeFixed> next;
std::weak_ptr<NodeFixed> prev; // weak_ptr
int value;
NodeFixed(int v) : value(v) {
std::cout << "NodeFixed " << value << " 생성" << std::endl;
}
~NodeFixed() {
std::cout << "NodeFixed " << value << " 소멸" << std::endl;
}
};
void testFixed() {
auto n1 = std::make_shared<NodeFixed>(1);
auto n2 = std::make_shared<NodeFixed>(2);
n1->next = n2;
n2->prev = n1; // weak_ptr은 참조 카운트 증가 안함
std::cout << "n1 참조 카운트: " << n1.use_count() << std::endl; // 1
std::cout << "n2 참조 카운트: " << n2.use_count() << std::endl; // 2
}
int main() {
std::cout << "=== 순환 참조 테스트 ===" << std::endl;
testCircular();
std::cout << "함수 종료 (소멸자 호출 안됨!)" << std::endl;
std::cout << "\n=== weak_ptr 테스트 ===" << std::endl;
testFixed();
std::cout << "함수 종료 (소멸자 호출됨)" << std::endl;
return 0;
}
출력:
=== 순환 참조 테스트 ===
Node 1 생성
Node 2 생성
n1 참조 카운트: 2
n2 참조 카운트: 2
함수 종료 (소멸자 호출 안됨!)
=== weak_ptr 테스트 ===
NodeFixed 1 생성
NodeFixed 2 생성
n1 참조 카운트: 1
n2 참조 카운트: 2
NodeFixed 2 소멸
NodeFixed 1 소멸
함수 종료 (소멸자 호출됨)
문제 3: unique_ptr을 함수에 전달
#include <memory>
#include <iostream>
// 방법 1: 소유권 이전
void takeOwnership(std::unique_ptr<int> ptr) {
std::cout << "소유권 이전: " << *ptr << std::endl;
}
// 방법 2: 참조로 전달 (소유권 유지)
void borrow(const std::unique_ptr<int>& ptr) {
std::cout << "참조: " << *ptr << std::endl;
}
// 방법 3: raw 포인터로 전달 (소유권 없음)
void observe(int* ptr) {
if (ptr) {
std::cout << "관찰: " << *ptr << std::endl;
}
}
int main() {
auto ptr = std::make_unique<int>(10);
// ❌ 컴파일 에러
// takeOwnership(ptr); // 복사 불가
// ✅ 소유권 이전
// takeOwnership(std::move(ptr)); // ptr은 nullptr이 됨
// ✅ 참조로 전달
borrow(ptr); // ptr 유지
// ✅ raw 포인터로 전달
observe(ptr.get()); // ptr 유지
std::cout << "ptr 유효: " << (ptr ? "yes" : "no") << std::endl;
return 0;
}
출력:
참조: 10
관찰: 10
ptr 유효: yes
6. 실전 예제: 리소스 관리자
#include <memory>
#include <vector>
#include <iostream>
#include <string>
class ResourceManager {
private:
std::vector<std::unique_ptr<std::string>> resources_;
public:
// 리소스 추가
void add(std::unique_ptr<std::string> resource) {
resources_.push_back(std::move(resource));
}
// 리소스 생성 및 추가
void create(const std::string& value) {
resources_.push_back(std::make_unique<std::string>(value));
}
// 리소스 가져오기 (소유권 이전)
std::unique_ptr<std::string> take(size_t index) {
if (index >= resources_.size()) return nullptr;
auto resource = std::move(resources_[index]);
resources_.erase(resources_.begin() + index);
return resource;
}
// 리소스 개수
size_t count() const {
return resources_.size();
}
// 리소스 출력
void print() const {
std::cout << "Resources (" << resources_.size() << "):" << std::endl;
for (size_t i = 0; i < resources_.size(); ++i) {
if (resources_[i]) {
std::cout << " [" << i << "]: " << *resources_[i] << std::endl;
} else {
std::cout << " [" << i << "]: (moved)" << std::endl;
}
}
}
};
int main() {
ResourceManager mgr;
// 리소스 추가
mgr.add(std::make_unique<std::string>("Resource 1"));
mgr.create("Resource 2");
mgr.create("Resource 3");
std::cout << "초기 상태:" << std::endl;
mgr.print();
// 리소스 가져오기
auto r = mgr.take(1);
std::cout << "\n가져온 리소스: " << *r << std::endl;
std::cout << "\n남은 리소스:" << std::endl;
mgr.print();
return 0;
}
출력:
초기 상태:
Resources (3):
[0]: Resource 1
[1]: Resource 2
[2]: Resource 3
가져온 리소스: Resource 2
남은 리소스:
Resources (2):
[0]: Resource 1
[1]: Resource 3
정리
핵심 요약
- unique_ptr: 독점 소유, 복사 불가, 이동 가능
- shared_ptr: 공유 소유, 참조 카운팅
- weak_ptr: 순환 참조 방지, 참조 카운트 증가 안함
- make_unique/make_shared: 예외 안전성, 성능 이점
- RAII: 자동 메모리 관리
스마트 포인터 비교
| 특징 | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| 소유권 | 독점 | 공유 | 없음 |
| 복사 | 불가 | 가능 | 가능 |
| 이동 | 가능 | 가능 | 가능 |
| 오버헤드 | 없음 | 참조 카운팅 | 없음 |
| 용도 | 기본 선택 | 공유 필요 | 순환 참조 방지 |
| 배열 | 지원 | 제한적 | - |
실전 팁
선택 가이드:
- 기본:
unique_ptr - 공유 필요:
shared_ptr - 순환 참조:
weak_ptr - 배열:
unique_ptr<T[]>또는vector
성능:
unique_ptr: raw 포인터와 동일shared_ptr: 참조 카운팅 오버헤드 (약간)make_shared: 메모리 할당 1번 (최적화)
주의사항:
make_unique/make_shared사용- 순환 참조 주의
- move 후 사용 금지
- 댕글링 포인터 방지
다음 단계
- C++ RAII
- C++ Move Semantics
- C++ weak_ptr
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
- C++ RAII & Smart Pointers | “스마트 포인터” 가이드
- C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]
관련 글
- C++ shared_ptr vs unique_ptr |
- C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
- C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열
- C++ 순환 참조 | shared_ptr 메모리 누수
- C++ RAII & Smart Pointers |