-
포인터는 C++ 입문자들에게 첫 번째 장벽이면서 C++ 개발자들이 평생 신경 써야 하는 존재입니다. Java와 C#은 자동으로 메모리를 관리해주지만 C++은 개발자가 관리해야 하죠. 하지만 Raw pointer을 전부 일일이 관리할 수 없기 때문에 스마트 포인터를 잘 활용해야 합니다.
스마트 포인터는 지역 변수의 생명 주기를 이용하는 클래스입니다. 소멸자를 통해서 메모리를 해제시켜주기 때문에 직접 delete를 이용하는 것보단 부담이 덜합니다. 여기서 중요한 것은 부담이 덜하다는 것이지 완전히 사라지는 것이 아니라는 점입니다. unique_ptr은 이름에서 알 수 있듯 포인터가 유일하게 하나만 존재할 수 있도록 복사를 막아놨습니다.
shared_ptr은 신경 써서 사용해야 합니다. 내부적으로 참조 카운트(_Uses라는 변수명)가 있어서 복사가 일어나면 참조 카운트가 늘어납니다. 참조하고 있던 변수가 사라지게 되면 참조 카운트가 하나 줄어듭니다. use_count() 함수로 참조 카운트를 조회할 수 있습니다. shared_ptr 객체가 만들어지는 순간에 참조 카운트는 1로 초기화됩니다. 0이 아닙니다. 0이 되면 관리하고 있던 포인터의 메모리가 해제됩니다.
문제는 동기화 과정에서 락이 순환이 생기면서 데드락이 생기듯, shared_ptr 역시 A 객체가 B를 참조하고 B객체가 A를 참조해버리면 참조 카운트가 영원히 0이 될 수 없어 메모리가 해제되지 않는 문제가 일어납니다. 이 문제를 해결하는 방법은 다양하며, weak_ptr을 이용해서도 해결할 수 있습니다. weak_ptr 객체는 이미 만들어진 weak_ptr 객체와 shared_ptr로만 초기화할 수 있습니다. shared_ptr 끼리 복사를 할 때는 아래 코드처럼 참조 카운트를 늘리지만
weak_ptr 객체를 만들 때는 참조 카운트를 늘리지 않습니다. 때문에 shared_ptr 객체 끼리 상호 참조를 해 참조 횟수가 2가 되었고 스코프를 탈출해도 횟수가 1에 그치는 바람에 메모리 해제가 안 되는 문제에 weak_ptr을 적용해보겠습니다. 상호 참조를 해도 참조 횟수가 1이 유지되기 때문에 스코프를 탈출하면 참조 횟수가 0이 되면서 해당 메모리를 해제할 수 있게 됩니다. shared_ptr의 소멸자에서 참조 횟수를 줄이는 함수가 호출됩니다.
_Destroy()는 내부적으로 갖고 있는 Raw 포인터인 _Ptr를 delete하며 _Ptr은 shard_ptr의 부모 클래스인 _Ptr_base 클래스의 멤버 변수입니다.
weak_ptr이 만능은 아닙니다. shared_ptr은 참조 카운트인 _Uses 외에도 weak_ptr를 참조하는 횟수인 _Weaks를 갖고 있습니다. _Uses가 0이 되면 _Ptr이 사라지면서 해당 shared_ptr를 참조하고 있던 weak_ptr 객체는 비어 있는 상태(empty)가 되는데요. weak_ptr 객체의 expired 함수로 조회할 수 있습니다. 메모리가 해제되면 expired 됩니다. weak_ptr은 _Weaks가 0이 되어야 스스로 해제할 수 있습니다.
_Weaks는 shared_ptr 객체가 생성될 때 _Uses와 함께 1이 되고, weak_ptr 객체를 새로 참조할 때마다 1이 늘어나며, 위 그림에서 알 수 있듯 참조 횟수를 줄이는 함수인 _Decref()가 호출될 때 -1 연산을 합니다. 그리고 _Weaks가 0이 되어야만 스스로 해제가 되기 때문에 스코프가 끝나는 시점에 _Weaks가 0이 되지 않는다면 메모리 누수가 발생하게 됩니다. 참고로 shared_ptr의 reset()을 호출할 때, 참조 카운트는 -1이 되지만 _Weaks는 변하지 않습니다.
'개발 > C·C++' 카테고리의 다른 글
바이트 정렬 (0) 2021.09.24 union (0) 2021.09.24 스레드 함수를 람다식 안에서 사용할 때 주의할 점 (0) 2021.09.15 split() (0) 2021.09.11 생산자/소비자 패턴에서의 WaitForSingleObject() (0) 2021.09.01