스마트 포인터와 메모리 관리
스마트 포인터와 메모리 관리¶
1. 메모리 관리의 어려움¶
C++에서 수동 메모리 관리는 여러 문제를 일으킬 수 있습니다.
#include <iostream>
// 메모리 누수 예제
void memoryLeak() {
int* p = new int(42);
// delete를 잊음 - 메모리 누수!
}
// 이중 해제 예제
void doubleFree() {
int* p = new int(42);
delete p;
// delete p; // 이중 해제 - 정의되지 않은 동작!
}
// 댕글링 포인터 예제
int* danglingPointer() {
int* p = new int(42);
delete p;
return p; // 해제된 메모리를 가리킴 - 위험!
}
// 예외 시 메모리 누수
void exceptionLeak() {
int* p = new int(42);
// throw std::runtime_error("Error!"); // delete 실행 안 됨
delete p;
}
문제점 정리¶
| 문제 | 설명 |
|---|---|
| 메모리 누수 | delete를 호출하지 않음 |
| 이중 해제 | 같은 메모리를 두 번 해제 |
| 댕글링 포인터 | 해제된 메모리 접근 |
| 예외 안전성 | 예외 발생 시 메모리 누수 |
2. RAII (Resource Acquisition Is Initialization)¶
자원 획득은 초기화다: 객체 생성 시 자원 획득, 소멸 시 자동 해제.
#include <iostream>
// RAII 원칙 적용 클래스
class IntPtr {
private:
int* ptr;
public:
// 생성자에서 자원 획득
explicit IntPtr(int value) : ptr(new int(value)) {
std::cout << "메모리 할당" << std::endl;
}
// 소멸자에서 자원 해제
~IntPtr() {
delete ptr;
std::cout << "메모리 해제" << std::endl;
}
int& operator*() { return *ptr; }
int* get() { return ptr; }
// 복사 금지 (단순화)
IntPtr(const IntPtr&) = delete;
IntPtr& operator=(const IntPtr&) = delete;
};
void useRAII() {
IntPtr p(42);
std::cout << "값: " << *p << std::endl;
// 함수 종료 시 자동으로 메모리 해제
}
int main() {
std::cout << "=== RAII 시작 ===" << std::endl;
useRAII();
std::cout << "=== RAII 끝 ===" << std::endl;
return 0;
}
출력:
=== RAII 시작 ===
메모리 할당
값: 42
메모리 해제
=== RAII 끝 ===
3. unique_ptr¶
단독 소유권을 가지는 스마트 포인터입니다. 하나의 unique_ptr만 객체를 소유할 수 있습니다.
기본 사용법¶
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource 생성" << std::endl; }
~Resource() { std::cout << "Resource 소멸" << std::endl; }
void use() { std::cout << "Resource 사용" << std::endl; }
};
int main() {
// unique_ptr 생성
std::unique_ptr<Resource> p1(new Resource());
p1->use();
// make_unique 사용 (C++14, 권장)
auto p2 = std::make_unique<Resource>();
p2->use();
// 기본 타입
auto num = std::make_unique<int>(42);
std::cout << "값: " << *num << std::endl;
// 배열
auto arr = std::make_unique<int[]>(5);
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
}
std::cout << "배열: ";
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
return 0; // 자동으로 모든 메모리 해제
}
소유권 이전 (move)¶
#include <iostream>
#include <memory>
void takeOwnership(std::unique_ptr<int> p) {
std::cout << "함수 내부: " << *p << std::endl;
} // p가 여기서 소멸
std::unique_ptr<int> createResource() {
return std::make_unique<int>(100);
}
int main() {
auto p1 = std::make_unique<int>(42);
// 복사 불가
// auto p2 = p1; // 컴파일 에러!
// 이동은 가능
auto p2 = std::move(p1);
std::cout << "p2: " << *p2 << std::endl;
// p1은 이제 nullptr
if (p1 == nullptr) {
std::cout << "p1은 비어있음" << std::endl;
}
// 함수에 전달 (소유권 이전)
auto p3 = std::make_unique<int>(200);
takeOwnership(std::move(p3));
// p3는 이제 nullptr
// 함수에서 반환 (소유권 이전)
auto p4 = createResource();
std::cout << "p4: " << *p4 << std::endl;
return 0;
}
unique_ptr 메서드¶
#include <iostream>
#include <memory>
int main() {
auto p = std::make_unique<int>(42);
// get(): 원시 포인터 얻기 (소유권 유지)
int* raw = p.get();
std::cout << "raw: " << *raw << std::endl;
// release(): 소유권 포기하고 원시 포인터 반환
int* released = p.release();
if (p == nullptr) {
std::cout << "p는 비어있음" << std::endl;
}
delete released; // 수동 해제 필요
// reset(): 기존 객체 해제하고 새 객체 설정
auto p2 = std::make_unique<int>(100);
std::cout << "reset 전: " << *p2 << std::endl;
p2.reset(new int(200));
std::cout << "reset 후: " << *p2 << std::endl;
p2.reset(); // nullptr로 설정
if (!p2) {
std::cout << "p2는 비어있음" << std::endl;
}
// swap(): 두 포인터 교환
auto a = std::make_unique<int>(1);
auto b = std::make_unique<int>(2);
a.swap(b);
std::cout << "swap 후: a=" << *a << ", b=" << *b << std::endl;
return 0;
}
커스텀 삭제자¶
#include <iostream>
#include <memory>
#include <cstdio>
// 함수 삭제자
void customDeleter(int* p) {
std::cout << "커스텀 삭제자 호출" << std::endl;
delete p;
}
// FILE* 용 삭제자
auto fileDeleter = [](FILE* f) {
if (f) {
std::cout << "파일 닫기" << std::endl;
fclose(f);
}
};
int main() {
// 함수 포인터 삭제자
std::unique_ptr<int, void(*)(int*)> p1(
new int(42), customDeleter
);
// 람다 삭제자
auto deleter = [](int* p) {
std::cout << "람다 삭제자" << std::endl;
delete p;
};
std::unique_ptr<int, decltype(deleter)> p2(
new int(100), deleter
);
// FILE 관리
std::unique_ptr<FILE, decltype(fileDeleter)> file(
fopen("test.txt", "w"), fileDeleter
);
if (file) {
fprintf(file.get(), "Hello, World!\n");
}
return 0;
}
4. shared_ptr¶
공유 소유권을 가지는 스마트 포인터입니다. 여러 shared_ptr이 같은 객체를 공유할 수 있습니다.
기본 사용법¶
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource 생성" << std::endl; }
~Resource() { std::cout << "Resource 소멸" << std::endl; }
};
int main() {
// shared_ptr 생성
std::shared_ptr<Resource> p1 = std::make_shared<Resource>();
std::cout << "참조 카운트: " << p1.use_count() << std::endl; // 1
{
// 공유
std::shared_ptr<Resource> p2 = p1;
std::cout << "참조 카운트: " << p1.use_count() << std::endl; // 2
std::shared_ptr<Resource> p3 = p1;
std::cout << "참조 카운트: " << p1.use_count() << std::endl; // 3
}
// p2, p3 소멸
std::cout << "참조 카운트: " << p1.use_count() << std::endl; // 1
return 0; // 참조 카운트가 0이 되면 Resource 소멸
}
make_shared의 장점¶
#include <iostream>
#include <memory>
class Widget {
public:
int data[100];
};
int main() {
// 방법 1: new 사용 (메모리 할당 2번)
std::shared_ptr<Widget> p1(new Widget());
// 방법 2: make_shared 사용 (메모리 할당 1번, 권장)
auto p2 = std::make_shared<Widget>();
/*
make_shared 장점:
1. 메모리 할당 1번 (객체 + 제어 블록)
2. 예외 안전성
3. 코드 간결
*/
std::cout << "p1 use_count: " << p1.use_count() << std::endl;
std::cout << "p2 use_count: " << p2.use_count() << std::endl;
return 0;
}
shared_ptr과 컨테이너¶
#include <iostream>
#include <memory>
#include <vector>
class Person {
public:
std::string name;
Person(const std::string& n) : name(n) {
std::cout << name << " 생성" << std::endl;
}
~Person() {
std::cout << name << " 소멸" << std::endl;
}
};
int main() {
std::vector<std::shared_ptr<Person>> people;
auto alice = std::make_shared<Person>("Alice");
auto bob = std::make_shared<Person>("Bob");
people.push_back(alice);
people.push_back(bob);
people.push_back(alice); // Alice 공유
std::cout << "Alice 참조 카운트: " << alice.use_count() << std::endl; // 3
std::cout << "\n=== 목록 ===" << std::endl;
for (const auto& p : people) {
std::cout << p->name << std::endl;
}
people.clear();
std::cout << "\n=== clear 후 ===" << std::endl;
std::cout << "Alice 참조 카운트: " << alice.use_count() << std::endl; // 1
return 0;
}
5. weak_ptr¶
shared_ptr의 순환 참조 문제를 해결합니다. 참조 카운트를 증가시키지 않습니다.
순환 참조 문제¶
#include <iostream>
#include <memory>
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() {
{
auto a = std::make_shared<A>();
auto 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;
}
weak_ptr로 해결¶
#include <iostream>
#include <memory>
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() {
{
auto a = std::make_shared<A>();
auto 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
}
// 정상적으로 소멸!
std::cout << "블록 종료" << std::endl;
return 0;
}
weak_ptr 사용법¶
#include <iostream>
#include <memory>
int main() {
std::weak_ptr<int> weak;
{
auto shared = std::make_shared<int>(42);
weak = shared;
std::cout << "블록 내부:" << std::endl;
std::cout << " expired: " << weak.expired() << std::endl; // false
std::cout << " use_count: " << weak.use_count() << std::endl; // 1
// weak_ptr 접근: lock()으로 shared_ptr 얻기
if (auto sp = weak.lock()) {
std::cout << " 값: " << *sp << std::endl;
}
}
// shared가 소멸됨
std::cout << "블록 외부:" << std::endl;
std::cout << " expired: " << weak.expired() << std::endl; // true
std::cout << " use_count: " << weak.use_count() << std::endl; // 0
if (auto sp = weak.lock()) {
std::cout << " 값: " << *sp << std::endl;
} else {
std::cout << " 객체가 소멸됨" << std::endl;
}
return 0;
}
캐시 구현 예제¶
#include <iostream>
#include <memory>
#include <map>
#include <string>
class Image {
public:
std::string filename;
Image(const std::string& fn) : filename(fn) {
std::cout << "이미지 로딩: " << filename << std::endl;
}
~Image() {
std::cout << "이미지 해제: " << filename << std::endl;
}
};
class ImageCache {
private:
std::map<std::string, std::weak_ptr<Image>> cache;
public:
std::shared_ptr<Image> getImage(const std::string& filename) {
auto it = cache.find(filename);
if (it != cache.end()) {
// 캐시에 있으면 weak_ptr에서 shared_ptr 얻기 시도
if (auto sp = it->second.lock()) {
std::cout << "캐시 히트: " << filename << std::endl;
return sp;
}
}
// 캐시 미스: 새로 로딩
std::cout << "캐시 미스: " << filename << std::endl;
auto image = std::make_shared<Image>(filename);
cache[filename] = image;
return image;
}
};
int main() {
ImageCache cache;
{
auto img1 = cache.getImage("photo.jpg");
auto img2 = cache.getImage("photo.jpg"); // 캐시 히트
auto img3 = cache.getImage("icon.png");
std::cout << "img1 use_count: " << img1.use_count() << std::endl;
}
// 모든 이미지 해제됨
std::cout << "\n=== 다시 요청 ===" << std::endl;
auto img = cache.getImage("photo.jpg"); // 다시 로딩
return 0;
}
6. enable_shared_from_this¶
클래스 내부에서 자신의 shared_ptr을 안전하게 얻습니다.
#include <iostream>
#include <memory>
#include <vector>
class Task : public std::enable_shared_from_this<Task> {
public:
std::string name;
Task(const std::string& n) : name(n) {
std::cout << name << " 생성" << std::endl;
}
~Task() {
std::cout << name << " 소멸" << std::endl;
}
// 자신의 shared_ptr를 안전하게 반환
std::shared_ptr<Task> getPtr() {
return shared_from_this();
}
void addToQueue(std::vector<std::shared_ptr<Task>>& queue) {
queue.push_back(shared_from_this());
}
};
int main() {
std::vector<std::shared_ptr<Task>> taskQueue;
{
auto task = std::make_shared<Task>("Task1");
std::cout << "참조 카운트: " << task.use_count() << std::endl; // 1
task->addToQueue(taskQueue);
std::cout << "참조 카운트: " << task.use_count() << std::endl; // 2
}
// task 변수는 소멸, 하지만 taskQueue에 남아있음
std::cout << "\n=== Queue 내용 ===" << std::endl;
for (const auto& t : taskQueue) {
std::cout << t->name << std::endl;
}
return 0;
}
주의사항:
// 잘못된 사용 - 반드시 shared_ptr로 관리되어야 함
// Task t("Direct");
// t.getPtr(); // 런타임 에러!
7. 스마트 포인터 선택 가이드¶
┌─────────────────────────────────────────────────────┐
│ 스마트 포인터 선택 │
└─────────────────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
단독 소유? 공유 필요? 약한 참조?
│ │ │
▼ ▼ ▼
unique_ptr shared_ptr weak_ptr
| 상황 | 선택 |
|---|---|
| 하나의 소유자 | unique_ptr |
| 여러 소유자 | shared_ptr |
| 순환 참조 방지 | weak_ptr |
| 캐시, 옵저버 | weak_ptr |
| 팩토리 함수 반환 | unique_ptr |
| 컨테이너 저장 | shared_ptr 또는 unique_ptr |
8. 스마트 포인터와 함수¶
함수 매개변수¶
#include <iostream>
#include <memory>
class Widget {
public:
int value;
Widget(int v) : value(v) {}
};
// 소유권 전달 (unique_ptr)
void takeOwnership(std::unique_ptr<Widget> w) {
std::cout << "소유권 받음: " << w->value << std::endl;
}
// 소유권 공유 (shared_ptr 복사)
void shareOwnership(std::shared_ptr<Widget> w) {
std::cout << "공유 받음: " << w->value
<< " (count: " << w.use_count() << ")" << std::endl;
}
// 소유권 없이 사용 (참조)
void useOnly(Widget& w) {
std::cout << "사용만: " << w.value << std::endl;
}
// 소유권 없이 사용 (원시 포인터)
void useOnlyPtr(Widget* w) {
if (w) {
std::cout << "포인터 사용: " << w->value << std::endl;
}
}
int main() {
// unique_ptr
auto up = std::make_unique<Widget>(1);
useOnly(*up);
useOnlyPtr(up.get());
takeOwnership(std::move(up)); // 소유권 이전
// shared_ptr
auto sp = std::make_shared<Widget>(2);
useOnly(*sp);
useOnlyPtr(sp.get());
shareOwnership(sp); // 공유
std::cout << "원본 count: " << sp.use_count() << std::endl;
return 0;
}
함수 반환¶
#include <iostream>
#include <memory>
class Product {
public:
std::string name;
Product(const std::string& n) : name(n) {}
};
// 팩토리 함수: unique_ptr 반환
std::unique_ptr<Product> createProduct(const std::string& name) {
return std::make_unique<Product>(name);
}
// 캐시된 객체: shared_ptr 반환
std::shared_ptr<Product> getCachedProduct() {
static auto cached = std::make_shared<Product>("Cached");
return cached;
}
int main() {
auto p1 = createProduct("Widget");
std::cout << p1->name << std::endl;
auto p2 = getCachedProduct();
auto p3 = getCachedProduct();
std::cout << "캐시 count: " << p2.use_count() << std::endl; // 3
return 0;
}
9. 일반적인 실수와 해결¶
실수 1: 같은 원시 포인터로 여러 스마트 포인터 생성¶
#include <iostream>
#include <memory>
int main() {
int* raw = new int(42);
// 잘못된 코드 - 절대 하지 말 것!
// std::shared_ptr<int> p1(raw);
// std::shared_ptr<int> p2(raw); // 이중 해제 발생!
// 올바른 코드
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // 공유
return 0;
}
실수 2: this를 shared_ptr로 변환¶
#include <iostream>
#include <memory>
class Bad {
public:
// 잘못된 방법
std::shared_ptr<Bad> getShared() {
// return std::shared_ptr<Bad>(this); // 위험!
return nullptr;
}
};
class Good : public std::enable_shared_from_this<Good> {
public:
// 올바른 방법
std::shared_ptr<Good> getShared() {
return shared_from_this();
}
};
실수 3: 순환 참조¶
// 위의 weak_ptr 섹션 참조
// shared_ptr만 사용하면 순환 참조로 메모리 누수
// weak_ptr로 한쪽 연결을 약한 참조로 변경
실수 4: unique_ptr 복사 시도¶
#include <memory>
void processWidget(std::unique_ptr<int> p) {}
int main() {
auto p = std::make_unique<int>(42);
// 잘못된 코드
// processWidget(p); // 컴파일 에러
// 올바른 코드 (소유권 이전)
processWidget(std::move(p));
return 0;
}
10. 성능 고려사항¶
unique_ptr vs shared_ptr¶
#include <iostream>
#include <memory>
#include <chrono>
#include <vector>
int main() {
const int N = 1000000;
// unique_ptr (오버헤드 거의 없음)
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; i++) {
auto p = std::make_unique<int>(i);
}
auto end1 = std::chrono::high_resolution_clock::now();
// shared_ptr (참조 카운트 관리 오버헤드)
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; i++) {
auto p = std::make_shared<int>(i);
}
auto end2 = std::chrono::high_resolution_clock::now();
auto dur1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start1);
auto dur2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - start2);
std::cout << "unique_ptr: " << dur1.count() << " us" << std::endl;
std::cout << "shared_ptr: " << dur2.count() << " us" << std::endl;
return 0;
}
메모리 구조¶
unique_ptr:
┌─────────────────┐
│ ptr → 객체 │ (포인터 하나만)
└─────────────────┘
shared_ptr:
┌─────────────────┐ ┌─────────────────┐
│ ptr ─────────────┬──▶│ 객체 │
│ control ───┐ │ └─────────────────┘
└─────────────│───┘
▼
┌─────────────────┐
│ 참조 카운트 │
│ weak 카운트 │
│ 삭제자 │
└─────────────────┘
11. 요약¶
| 스마트 포인터 | 소유권 | 복사 | 참조 카운트 | 용도 |
|---|---|---|---|---|
unique_ptr |
단독 | X | X | 단일 소유자 |
shared_ptr |
공유 | O | O | 공유 소유권 |
weak_ptr |
없음 | O | X | 순환 참조 방지 |
핵심 원칙¶
- new/delete 직접 사용 피하기 -
make_unique,make_shared사용 - 기본은 unique_ptr - 필요할 때만 shared_ptr
- 순환 참조 주의 - weak_ptr로 해결
- RAII 원칙 준수 - 자원 관리 자동화
12. 연습 문제¶
연습 1: 리소스 매니저¶
파일, 네트워크 연결 등 다양한 리소스를 관리하는 클래스를 unique_ptr로 구현하세요.
연습 2: 그래프 자료구조¶
노드들이 서로 연결된 그래프를 shared_ptr와 weak_ptr로 구현하세요.
연습 3: 객체 풀¶
재사용 가능한 객체 풀을 스마트 포인터로 구현하세요.
다음 단계¶
15_Modern_CPP.md에서 C++11/14/17/20의 주요 기능을 배워봅시다!