c++를 코딩을 하며 가장 힘들다고 느끼는 점은 역시 메모리 관리 부분인 것 같다. c++은 java, c#등과 다르게 GC(가비지 콜렉터)가 존재하지 않는다. 만약 new로 할당해준 메모리를 delete해주지 않는다면 해당 메모리는 프로그램이 종료될 때까지 삭제되지 않고 계속 남아있게 된다. 이를 메모리 누수 (Memory Leak) 이라고 부른다. 메모리 누수가 발생하면 이런저런 오류가 생길 뿐 아니라 프로그램 자체가 뻗을 수도 있기에 c++에서 메모리 관리는 정말 중요하다고 할 수 있다.
하지만, 매번 새로운 메모리를 할당할 때 마다 new delete를 해주는 것도 여간 귀찮은 일이 아니다... 이런 귀찮음을 해결하기 위해서는 자동으로 호출되는 소멸자에 의해 자동으로 소멸되는 객체를 만들어주면 된다.
c++에서 만들어준 이 객체가 바로 스마트 포인터이다.
unique_ptr은 이름에서부터 알아볼 수 있듯이 특별한 포인터이다. unique_ptr은 객체에 대해서 유일한 소유권을 부여하는데 이 말이 무슨 말이냐 하면
우선 일반적으로 다음과 같은 코드에서는 오류가 뜰 수 밖에 없다.
C* c1 = new C;
C* c2 = c1;
delete c1;
delete c2;
두 포인터는 서로 같은 객체를 가르키고 있는데, 이미 한번 소멸된 객체를 한번 더 소멸하려고 하기 때문에 double free오류가 발생한다.
unique_ptr은 말했던 것 처럼 특정 객체에 유일한 소유권을 부여하여 이러한 문제를 예방한다.
C* c = new C();
unique_ptr<C> ptr1(c);
unique_ptr<C> ptr2 = ptr1; // error
unique_ptr에서 같은 값을 참조하려고 하면 에러를 뱉어내는 것을 볼 수 있다.
만약 새로운 unique_ptr로 소유권을 이전시켜주고 싶다면 std::move함수를 이용하여 기존 unique_ptr의 소유권을 이전시켜줄 수 있다.
C* c = new C();
unique_ptr<C> ptr1(c);
unique_ptr<C> ptr2 = std::move(ptr1);
항상 새로운 객체를 생성해주고 넣어주는 방식 대신 make_unique함수를 사용할 수도 있다.
// 지정된 형식의 unique_ptr객체를 만들고 반환하는 함수
unique_ptr<C> ptr1 = make_unique<C>();
shared_ptr은 여러 개의 shared_ptr이 하나의 객체를 가르킬 수 있다. 때문에 unique_ptr과는 반대라고도 할 수 있다.
shared_ptr은 해당 객체를 가르키고 있는 shared_ptr의 개수 즉 참조 개수를 통하여 객체를 관리하고 참조 개수가 0이 되면 객체를 소멸시킨다.
vector<shared_ptr<C>> v;
shared_ptr<C> ptr = make_shared<C>();
cout << ptr.use_count() << " > "; // 참조 개수 출력
v.push_back(ptr);
cout << ptr.use_count() << " > "; // 참조 개수 출력
v.push_back(ptr);
cout << ptr.use_count(); // 참조 개수 출력
output
1 > 2 > 3
객체를 가르키는 shared_ptr을 추가할수록 객체의 참조 개수가 증가하는 것을 볼 수 있다.
v.pop_back();
cout << ptr.use_count() << " > "; // 참조 개수 출력
v.pop_back();
cout << ptr.use_count() << " > "; // 참조 개수 출력
output
2 > 1
반대로 shared_ptr을 제거하면 참조 개수가 줄어든다.
0이 출력되지 않는 이유는 아직 ptr
변수가 객체를 가르키고 있기 때문
shared_ptr은 실제로 정말 편리하게 사용할 수 있지만, 잘 못 사용하면 순환참조 (Circular Reference) 문제가 발생할 수 있다. 순환참조는 두 개의 shared_ptr이 서로 참조하고 있는 상태를 말한다.
이 문제는 shared_ptr의 고질적인 문제이며, shared_ptr만으로는 해결할 수 없다. 이 문제를 해결하기 위해서는 다음에 소개할 weak_ptr을 사용해야 한다.
직역하면 약한 포인터로 위에서 말한것 처럼 shared_ptr의 고질적인 문제인 순환참조를 해결하기 위해 나온 스마트 포인터이다.
이름과 같이 weak_ptr은 shared_ptr에 의존적인 스마트 포인터이다. weak_ptr은 객체 자체를 직접 가르키지 못하고 shared_ptr 또는 다른 weak_ptr을 가르킬 수 있다. 또한 가르키는 객체에 직접 접근할 수도 없다.
shared_ptr<C> ptr = make_shared<C>();
weak_ptr<C> weak1(ptr); // shared_ptr을 참조하기
weak_ptr<C> weak2(weak1); // weak_ptr을 참조하기
weak_ptr<C> weak3(new C()); // error
이처럼 weak_ptr은 객체를 직접 가르킬 수 없다. 하지만 접근조차 하지 못한다면 weak_ptr은 어떻게 사용하여야 할가?
weak_ptr이 가르키는 객체에 접근하기 위해서는 weak_ptr을 shared_ptr로 변환하여 사용하여야 한다. 이 때 lock()을 사용한다.
lock은 weak_ptr이 가르키는 객체가 소멸되지 않았다면 shared_ptr을 이미 소멸되었다면 빈 shared_ptr을 반환하는 함수이다.
shared_ptr<C> ptr1 = make_shared<C>();
weak_ptr<C> ptr2(ptr1);
shared_ptr<C> temp_ptr = ptr2.lock();
if(temp_ptr) {
temp_ptr->Hello();
}
else {
cout << "가르키는 객체가 존재하지 않음";
}
이와 같이 lock을 사용하여 weak_ptr을 shared_ptr로 변환할 수 있다.