오늘은 C++ 단골 문제, 스마트 포인터에 대해 다뤄 보려 한다.
왜 쓰는가?
일단 왜 쓰는가를 알아야 한다.
RAII ( Resource Acguisition Is Initialization ) 직역 하자면 자원 흭득을 초기화 한다.
⇒ Object(Smart Pointer)와 Resource (Heap Memory)의 Life Cycle을 일치 시킨다.
라는 개념으로 간단히 정리하자면.
동적으로 객체 생성 후 delete를 해주지 않아 Heap Memory에 쌓이는 Memory Leak ( 메모리 누수 ) 현상을 원천적으로 차단 해줄 수 있는 이점이 있기 때문이다.
코드
#include <iostream> #include <memory> #include <vector> using namespace std; class Cat { public: Cat(int age):_age(age) { cout << "cat constructor" << '\n'; } ~Cat() { cout << "cat destructor" << '\n'; } private: int _age; }; int main() { Cat * ptr = new Cat{3}; return 0; }
현재 그림을 코드로 설명해보자면 위와 같이 Stack Memory에 Ptr을 정해주고
Cat은 Heap Memory 에 넣게 된다.
이 때, Stack에 있는 데이터들은 함수가 종료 되면 사라진다는 특징을 갖고 있어
생성 후, 삭제 되는 것은 Cat이 아닌 Heap Memory에 저장된 Cat을 가르키고 있는
Ptr만 삭제 된다.
즉 이처럼, 생성자가 실행 되었다는 것을 알 수 있지만 소멸자는 호출이 되지 않았고 Heap Memory에 있는 Cat은 이제 프로세스가 종료될 때 까지 영영 지워지지 않는다.
해당 현상을 Memory Leak ( 메모리 누수 ) 라고 부른다.
그러면 delete를 잘해주지 않으면 안되나? 싶겠지만 아무리 훌륭한 개발자라 한들 실수를 하게 되고
이 실수가 치명적인 오류로 번질 수 있기 때문에 이러한 누수를 사전에 방지하고자 Smart Pointer가
등장하게 된다.
Unique Pointer
그럼 우선 스택이 종료되면 알아서 Heap 저장된 객체도 삭제 해주는 기능이 유니크 포인터라고
할 수 있다. 일단 이 녀석의 특징은 2가지와 같다.
- 포인터가 Stack에서 지워지면 가르키고 있던 Heap에 저장된 Object를 deallocation 해준다.
- 유니크 PTR은 {}(스코프) 중심의 라이플 사이클을 가지고 있다
- Exclusive Owner Ship 특징을 가진다 ( 간단히, 하나의 Object를 가르킬 수 있는 포인터는 하나로 강제 한다 )
#include <iostream> #include <memory> #include <vector> using namespace std; class Cat { public: Cat(){ cout << "cat constructor" << '\n'; } Cat(int age):_age(age) { cout << "cat constructor" << '\n'; } ~Cat() { cout << "cat destructor" << '\n'; } private: int _age; }; int main() { //vector<Cat> cats(5); cout << " 시작 " << '\n'; { unique_ptr<Cat> cat = make_unique<Cat>(3); } cout << " 끝 " << '\n'; return 0; }
위와 같이 본래라면 main 함수가 끝나서 Stack에 있는 메모리가 삭제 될 때 소멸자가 호출되어야 하지만 스코프 기반으로 라이플 사이클이 동작하기 때문에, 스코프가 끝나는 시점에서 소멸자가
호출 되는 것을 볼 수 있다.
Vector
Vector와 같은 컨테이너는 내부적으로 Stack이 끝나면, 자동으로 deallocation을 시켜준다. 주석으로 처리 된 부분을 풀면 확인 할 수 있다.
Shared Pointer
Unique Pointer의 Exclusive Owner Ship 특징과는 반대로 공유 포인터 즉, 여러 포인터가 하나의
오브젝트를 가르킬 수 있다. 단 스마트 포인터의 경우 RAII를 제공해야 하는데.
언제 오브젝트를 해제 해야 할지는 reference Count를 통해 알 수 있다.
즉, Object를 가르키는 Shared Pointer의 개 수를 통해 0 이 되면 소멸자를 호출하는 구조다.
— 주의 점
- 해당 객체의 멤버 변수로, 포인터가 자기 자신을 가르킬 경우 memory leak이 발생한다.
즉 위와 같은 구조를 의미하며 이 때, Stack에서 PTR이 사라지면, 당연히 Heap에서 PTR은 남아 있어
레퍼런스 카운터는 프로세스가 종료 될 때 까지 1을 Count 하면서 Leak이 발생하는 것이다.
- (보통 하는 실수) Circluar Reference
#include <iostream> #include <memory> #include <vector> using namespace std; class Cat { public: Cat(){ cout << "cat constructor" << '\n'; } ~Cat() { cout << "cat destructor" << '\n'; } shared_ptr<Cat> Mfriend; private: int _age; }; int main() { shared_ptr<Cat> pKitty = make_shared<Cat>(); shared_ptr<Cat> pNabbiy = make_shared<Cat>(); pKitty->Mfriend = pNabbiy; pNabbiy->Mfriend = pKitty; return 0; }
코드를 보면 서로 Mfirend가 서로를 가르키고 있다 이러면 Stack이 끝나게 되어도
Heap에서 원형적으로 포인터가 서로를 가르키고 있어서 Memory Leak이 일어난다.