Raw Pointer의 문제점
class Cat
{
public:
Cat() : mAge{0}
{
std::cout << "Cat constructor" << std::endl;
}
~Cat()
{
std::cout << "Cat destructor" << std::endl;
}
private:
int mAge;
};
void foo(Cat* ptr)
{
Cat* fooPtr = ptr;
delete fooPtr; // ptr이 가리키는 객체 해제
}
int main()
{
Cat* catPtr = new Cat();
Cat* catPtr1 = catPtr; // 같은 객체를 가리킴
foo(catPtr); // 객체 해제 (1번)
delete catPtr; // ❌ 이미 해제된 객체를 다시 해제! (Double Delete)
// Undefined Behavior 발생!
return 0;
}
발생 가능한 문제들
1. Memory Leak: delete를 잊어버림
2. Double Delete: 같은 메모리를 두 번 해제
3. Dangling Pointer: 해제된 메모리를 가리키는 포인터
4. 소유권 불명확: 누가 메모리를 해제해야 하는지 모호함
RAII란?
RAII를 통한 자동 메모리 관리
{
std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
// Cat 객체 생성 및 소유권 설정
// catPtr 사용...
} // ✅ 스코프를 벗어나면 자동으로 delete 호출!
// 명시적 delete 불필요
스마트 포인터의 장점
unique_ptr이란?
기본 사용법
#include <memory>
int main()
{
// ✅ 생성 방법 (C++14 이상)
std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
// C++11 방식
std::unique_ptr<Cat> catPtr2{new Cat()};
// 사용
catPtr->someMethod();
(*catPtr).someMethod();
// ✅ 자동으로 delete 호출됨
return 0;
}
복사 불가능, 이동 가능
std::unique_ptr<Cat> catPtr1 = std::make_unique<Cat>();
// ❌ 복사 불가능
std::unique_ptr<Cat> catPtr2 = catPtr1; // 컴파일 에러!
// ✅ 이동 가능 (소유권 이전)
std::unique_ptr<Cat> catPtr3 = std::move(catPtr1);
// catPtr1은 이제 nullptr
// catPtr3이 객체의 소유권을 가짐
함수 인자로 전달
// ❌ 값으로 전달 (소유권 이전, 원본은 nullptr)
void foo(std::unique_ptr<Cat> ptr)
{
// ptr이 소유권을 가짐
} // 여기서 객체 자동 해제
// ✅ 참조로 전달 (소유권 유지, 일시적 사용)
void bar(const std::unique_ptr<Cat>& ptr)
{
// ptr 사용
} // 소유권은 호출자가 계속 유지
// ✅ Raw 포인터로 전달 (소유권과 무관하게 사용)
void baz(Cat* ptr)
{
// ptr 사용
}
int main()
{
auto catPtr = std::make_unique<Cat>();
// foo(catPtr); // ❌ 복사 불가
foo(std::move(catPtr)); // ✅ 소유권 이전, catPtr은 nullptr
auto catPtr2 = std::make_unique<Cat>();
bar(catPtr2); // ✅ 소유권 유지
baz(catPtr2.get()); // ✅ Raw 포인터 전달
return 0;
}
클래스 멤버로 사용의 장점
class Zoo
{
public:
Zoo() : mCat{std::make_unique<Cat>()} {}
// ✅ 소멸자에서 자동으로 delete 호출됨
~Zoo() = default; // 명시적 delete 불필요!
private:
std::unique_ptr<Cat> mCat; // 자동 메모리 관리
};
주의사항: Copy 불가능
class Zoo
{
public:
Zoo() : mCat{std::make_unique<Cat>()} {}
private:
std::unique_ptr<Cat> mCat;
};
int main()
{
Zoo zoo1;
Zoo zoo2 = zoo1; // ❌ 컴파일 에러!
// unique_ptr은 복사 불가능하므로
// default copy constructor가 delete됨
return 0;
}
해결 방법 1: Copy Constructor/Assignment Delete
class Zoo
{
public:
Zoo() : mCat{std::make_unique<Cat>()} {}
// ✅ 명시적으로 복사 금지
Zoo(const Zoo&) = delete;
Zoo& operator=(const Zoo&) = delete;
// ✅ 이동은 허용
Zoo(Zoo&&) = default;
Zoo& operator=(Zoo&&) = default;
private:
std::unique_ptr<Cat> mCat;
};
해결 방법 2: Deep Copy 구현
class Zoo
{
public:
Zoo() : mCat{std::make_unique<Cat>()} {}
// Deep copy constructor
Zoo(const Zoo& other)
: mCat{other.mCat ? std::make_unique<Cat>(*other.mCat) : nullptr}
{
}
// Deep copy assignment
Zoo& operator=(const Zoo& other)
{
if (this != &other)
{
mCat = other.mCat ? std::make_unique<Cat>(*other.mCat) : nullptr;
}
return *this;
}
private:
std::unique_ptr<Cat> mCat;
};
unique_ptr 사용 가이드라인
unique_ptr 고려std::unique_ptr<int[]> (하지만 std::vector 권장)shared_ptr이란?
shared_ptr이 소멸될 때 객체 자동 해제기본 사용법
#include <memory>
int main()
{
// ✅ 생성 (C++11 이상)
std::shared_ptr<Cat> catPtr1 = std::make_shared<Cat>();
{
// 복사 가능 (참조 카운트 증가)
std::shared_ptr<Cat> catPtr2 = catPtr1; // ✅ OK
std::shared_ptr<Cat> catPtr3 = catPtr1;
// 참조 카운트 확인
std::cout << catPtr1.use_count() << std::endl; // 3
} // catPtr2, catPtr3 소멸 (참조 카운트 감소)
std::cout << catPtr1.use_count() << std::endl; // 1
return 0;
} // catPtr1 소멸, 참조 카운트 0 → Cat 객체 자동 해제
참조 카운팅 동작 원리
std::shared_ptr<Cat> ptr1 = std::make_shared<Cat>(); // count: 1
std::shared_ptr<Cat> ptr2 = ptr1; // count: 2 (복사)
std::shared_ptr<Cat> ptr3 = ptr2; // count: 3 (복사)
ptr3.reset(); // count: 2 (ptr3이 소유권 포기)
ptr2 = nullptr; // count: 1
// ptr1만 남음
// ptr1이 소멸되면 count: 0 → 객체 자동 해제
Control Block
shared_ptr 구조:
┌───────────────┐
│ Data Pointer │ → Cat 객체
├───────────────┤
│ Control Block│ → ┌──────────────────┐
└───────────────┘ │ Reference Count │
│ Weak Count │
│ Deleter │
└──────────────────┘
shared_ptr은 실제 객체 외에 Control Block이라는 추가 메모리 필요문제 상황: 얕은 복사
class Zoo
{
public:
Zoo() : mCat{std::make_shared<Cat>()} {}
// Default copy constructor 사용
// Zoo(const Zoo& other) = default;
private:
std::shared_ptr<Cat> mCat;
};
int main()
{
Zoo zoo1;
Zoo zoo2 = zoo1; // ✅ 복사 가능 (shared_ptr은 복사 가능)
// ❌ 문제: zoo1과 zoo2가 같은 Cat 객체를 공유!
// zoo1에서 Cat을 수정하면 zoo2에도 영향
return 0;
}
해결 방법 1: 주석으로 명시
class Zoo
{
public:
Zoo() : mCat{std::make_shared<Cat>()} {}
// ⚠️ Warning: This class uses shared_ptr.
// Copy constructor performs shallow copy.
// Both instances will share the same Cat object.
private:
std::shared_ptr<Cat> mCat;
};
해결 방법 2: Deep Copy 함수 제공
class Zoo
{
public:
Zoo() : mCat{std::make_shared<Cat>()} {}
// Shallow copy는 default 사용
Zoo(const Zoo&) = default;
Zoo& operator=(const Zoo&) = default;
// ✅ Deep copy를 위한 별도 함수 제공
Zoo deepCopy() const
{
Zoo newZoo;
newZoo.mCat = std::make_shared<Cat>(*mCat); // 새로운 Cat 객체 생성
return newZoo;
}
private:
std::shared_ptr<Cat> mCat;
};
int main()
{
Zoo zoo1;
Zoo zoo2 = zoo1; // Shallow copy
Zoo zoo3 = zoo1.deepCopy(); // Deep copy ✅
return 0;
}
해결 방법 3: Deep Copy를 기본으로 구현
class Zoo
{
public:
Zoo() : mCat{std::make_shared<Cat>()} {}
// Deep copy constructor
Zoo(const Zoo& other)
: mCat{other.mCat ? std::make_shared<Cat>(*other.mCat) : nullptr}
{
}
// Deep copy assignment
Zoo& operator=(const Zoo& other)
{
if (this != &other)
{
mCat = other.mCat ? std::make_shared<Cat>(*other.mCat) : nullptr;
}
return *this;
}
private:
std::shared_ptr<Cat> mCat;
};
Memory Leak 발생 상황
class Node
{
public:
std::shared_ptr<Node> next; // 다음 노드를 가리킴
~Node()
{
std::cout << "Node destroyed" << std::endl;
}
};
int main()
{
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node1 → node2 (node2 count: 2)
node2->next = node1; // node2 → node1 (node1 count: 2)
// ❌ 순환 참조 발생!
// node1, node2가 스코프를 벗어나도
// 서로를 참조하고 있어서 count가 0이 되지 않음
// → Memory Leak!
return 0;
}
// "Node destroyed" 출력되지 않음!
순환 참조 메모리 구조
node1 (count: 2) → next → node2 (count: 2)
↑ ↓
└────────── next ──────────┘
main 함수 종료 후:
node1 (count: 1) → next → node2 (count: 1)
↑ ↓
└────────── next ──────────┘
count가 모두 1이므로 해제되지 않음!
다른 언어의 해결 방법: Garbage Collection
// Java, C#, Python 등
// GC가 Mark and Sweep 알고리즘으로 순환 참조 감지 및 해제
// 1. Mark: 루트에서 도달 가능한 객체 표시
// 2. Sweep: 표시되지 않은 객체 해제
C++에는 GC가 없으므로 weak_ptr로 해결해야 함
weak_ptr이란?
shared_ptr로 변환 필요순환 참조 해결
class Node
{
public:
std::shared_ptr<Node> next; // 강한 참조
std::weak_ptr<Node> prev; // ✅ 약한 참조 (카운트 증가 X)
~Node()
{
std::cout << "Node destroyed" << std::endl;
}
};
int main()
{
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node1 → node2 (node2 count: 2)
node2->prev = node1; // node2 ⇢ node1 (node1 count: 1, weak!)
// ✅ 순환 참조 해결!
// node1 count: 1, node2 count: 2
return 0;
}
// node1 소멸 → node1 count: 0 → node1 해제
// node2 소멸 → node2 count: 0 → node2 해제
// "Node destroyed" 두 번 출력됨! ✅
weak_ptr 사용 방법
class Observer
{
public:
void observe(std::shared_ptr<Cat> cat)
{
mWeakCat = cat; // weak_ptr에 저장 (카운트 증가 X)
}
void check()
{
// weak_ptr을 shared_ptr로 변환 (lock)
if (auto sharedCat = mWeakCat.lock()) // ✅ 객체가 존재하면
{
// 안전하게 사용
sharedCat->meow();
}
else
{
std::cout << "Cat is gone!" << std::endl; // ❌ 객체가 이미 해제됨
}
}
private:
std::weak_ptr<Cat> mWeakCat; // 약한 참조
};
int main()
{
Observer observer;
{
auto cat = std::make_shared<Cat>();
observer.observe(cat);
observer.check(); // ✅ "Cat exists"
} // cat 소멸
observer.check(); // ❌ "Cat is gone!"
return 0;
}
weak_ptr 주요 메서드
std::weak_ptr<Cat> weakPtr;
// shared_ptr로 변환 (객체가 존재하면 shared_ptr 반환, 아니면 nullptr)
std::shared_ptr<Cat> sharedPtr = weakPtr.lock();
// 객체가 해제되었는지 확인
bool expired = weakPtr.expired(); // true면 객체 해제됨
// 참조 카운트 확인
long count = weakPtr.use_count(); // shared_ptr의 참조 카운트
언제 어떤 스마트 포인터를 사용할까?
| 상황 | 추천 포인터 |
|---|---|
| 기본적으로 | unique_ptr |
| 소유권이 명확한 경우 | unique_ptr |
| 소유권을 공유해야 하는 경우 | shared_ptr |
| 순환 참조 방지 | weak_ptr |
| 캐시 구현 | weak_ptr |
| Observer 패턴 | weak_ptr |
| 배열 관리 | std::vector (스마트 포인터보다 권장) |
일반적인 사용 패턴
// ✅ 1순위: unique_ptr (독점 소유)
std::unique_ptr<Cat> catPtr = std::make_unique<Cat>();
// ✅ 2순위: shared_ptr (공유 필요)
std::shared_ptr<Cat> catPtr = std::make_shared<Cat>();
// ✅ 3순위: weak_ptr (순환 참조 방지, 관찰만)
std::weak_ptr<Cat> weakCat = sharedCat;
// ❌ Raw pointer는 최후의 수단
Cat* rawPtr = new Cat(); // 피하기!
delete rawPtr;
성능 비교
// Raw pointer: 8 bytes
Cat* raw;
// unique_ptr: 8 bytes (오버헤드 없음)
std::unique_ptr<Cat> unique;
// shared_ptr: 16 bytes (포인터 8 + Control Block 포인터 8)
// + Control Block 16-24 bytes
std::shared_ptr<Cat> shared;
// weak_ptr: 16 bytes (shared_ptr과 동일)
std::weak_ptr<Cat> weak;
unique_ptr
shared_ptr
weak_ptr
lock()으로 shared_ptr 변환 필요Best Practices
1. Raw pointer 대신 스마트 포인터 사용
2. 기본은 unique_ptr, 필요시 shared_ptr
3. 순환 참조는 weak_ptr로 해결
4. make_unique/make_shared 사용 권장
5. 멤버 변수로 사용 시 복사 의미 명확히
코드없는 프로그래밍
C++ Smart Pointers
RAII
잘 보고 갑니다!