스마트 포인터
이 포스트는 C++11의 shared_ptr에 대한 것입니다.
shared_ptr의 사용법, 장점, 단점등을 살펴볼 것입니다. 보통 shared_ptr과 같은 스마트 포인터의 장점만을 인지하기 쉬운데, 이 포스트를 통해 단점에 대한 의견 자유롭게 나눌 수 있게 된다면 좋겠습니다.
스마트 포인터?
먼저 shared_ptr 같은 객체를 의미하는 스마트 포인터부터 설명을 하지 않을 수 없습니다. 스마트 포인터는 동적 메모리 할당 시 delete를 하지 않아 메모리 누수현상을 방지하기 위해 고안된 것입니다. ‘자동으로 메모리를 관리해주는 포인터’가 가장 잘 들어맞는 말인 것 같습니다. 밑의 예제와 같은 상황에서 스마트 포인터를 사용한다면 유용한 실수 방지 효과를 보실 수 있습니다.
normal_pointer가 가리키는 할당된 힙메모리를 해제해 주지 않은 채로 다른 주소를 대입하고 있습니다. 나중에 다시 쓸 메모리라면 다른 포인터에 저장해두어야 하고, 필요 없는 메모리라면 delete를 해주는 것이 바람직함에도 불구하고, 이렇게 실수를 하는 경우가 자주 있습니다. 동적 할당을 많이 사용하고 포인터를 자주 교환하다보면 메모리를 완벽하게 관리할 수 있다는 믿음은 오만일지도 모릅니다.
메모리가 제대로 해제되는지 알아보기 위해 소멸자에 출력 명령어를 넣었습니다. 위와 같이 스마트 포인터를 이용한다면 메모리를 따로 해제해주는 수고를 덜 수 있습니다. 메모리 해제시점을 고민하지 않아도 되므로 다른 코드에 집중을 더 할 수 있게 됩니다.
스마트 포인터는 <memory>헤더에 정의되어있습니다.
unique_ptr
unique_ptr에 대해서는 짤막한 소개만하고 넘어가겠습니다. unique_ptr은 힙메모리를 스택메모리처럼 다룬다는 개념입니다. 스택에 저장된 메모리는 스코프가 종료되면 같이 해제되는데, unique_ptr역시 스코프를 벗어날 때 참조된 메모리를 해제해줍니다.
uniqeu_ptr은 위와 같이 스코프안에 할당된 스택메모리, 포인터와 다를 바가 없습니다. 스택에 메모리를 할당하고 보통 포인터로 주소를 얻는 방법을 간단히 줄였다고 생각해도 지나치지 않습니다. 한 가지 차이점은 일반 포인터로는 normal_pointer2를 만들어 normalpointer2=&stack;을 해도 아무런 제약이 없지만, unique_ptr은 메모리의 소유권을 혼자서 차지한다는 데에 있습니다. unique의 모든 의미가 축약된 듯합니다. 소유권도 혼자, 스코프 안에서만 존재.
shared_ptr 장점
shared_ptr은 unique_ptr과 달리 많은 shared_ptr들이 동일한 메모리를 공유할 수 있습니다. 아무도 메모리를 가리키지 않을 경우 알아서 메모리가 해제되는 방식으로 동작하는데, 이런 기능을 구현하기 위해서는 레퍼런스 카운팅을 필요로 합니다. 얼마나 많은 shared_ptr들이 동일한 메모리를 가리키고 있는지 카운트하는 것으로, 카운팅이 0이되면 메모리를 해제해주는 것입니다. 결국에는 여러 곳에서 사용하다가 마지막으로 사용이 끝나면 해제되는 것으로 기존에 이용했던 포인터처럼 해제되기 전까지 객체를 스코프제한 없이 사용하면 됩니다.
shared2를 nullptr로 만듦으로써 레퍼런스 카운팅이0이되고 소멸자가 호출된 것을 볼 수 있습니다. shared_ptr의 장점은 메모리를 자동으로 해제해주는 힙메모리 포인터로 요약됩니다.
shared_ptr 사용법
위 사진의 코드를 보면 shared_ptr 사용법이 이미 나와 있습니다. 파라미터로 동적 할당 메모리주소를 넘겨주면 그 메모리를 관리하는 스마트 포인터를 얻게 됩니다. 만약 new 키워드를 쓰지 않거나 파라미터가 잘못된 경우 해당 선언 line을 무시하고 컴파일 됩니다. 컴파일러가 걸러주지 않는 것이라고 그냥 넘어가지 말고 키워드를 붙여두는 습관을 들이는 것이 역시 좋겠습니다.
그런데 만약, shared_ptr 객체에 메모리를 나중에 할당받고 싶은 경우 파라미터로 전달할 수가 없습니다. 일반포인터라면 나중에 new를 하거나, &연산자를 이용하여 주소를 넘겨주면 되지만, shared_ptr은 make_shared()함수를 이용해주면 됩니다.
위의 코드에는 파라미터로 new 키워드를 이용한 메모리 주소를 넘겨주는 방법과 make_shared를 이용한 방법이 있습니다. shared_ptr의 유용성을 이 코드에서도 볼 수 있는데, Myclass(‘a’)를 더 이상 쓰지 않고 Myclass(‘b’)를 이용하고 싶을 경우 별도로 delete를 하지 않고 바로 주소를 받아 사용할 수 있다는 것을 알 수 있습니다. a의 소멸자가 호출되고, shared1의 b, shared2의 c가 차례로 호출된 것을 볼 수 있습니다.
파라미터로 직접 new 키워드를 써서 동적메모리 주소를 직접 넘겨주지 않고도 위와 같이 파라미터가 아닌 다른 곳에서 할당한 주소를 인자로 넘겨주어도 됩니다.
shared_ptr 단점
shared_ptr객체는 *연산자과 ->연산자를 오버로딩 함으로써 일반적인 포인터를 다루듯이 사용할 수 있습니다. 메모리 해제도 알아서 해주니 메모리릭 현상을 방지할 수도 있음은 편리할 따름입니다. 그러나 만능인 것만 같은 shared_ptr에게도 미미한 단점은 있습니다.
첫 번째 단점은 스택 주소를 담을 수 없다는 것입니다.
위의 코드에서 Myclass stack(‘s’)의 주소를 넘겨주게 되면 메모리 중복해제로 인해 런타임에러가 발생됩니다. main() 스코프가 끝나면서 Myclass stack(‘s’)가 해제되고, 스마트 포인터가 한 번 더 해제하기 때문입니다. 따라서 일반적인 포인터 사용과 같이 스택 주소를 넘겨줄 수는 없고, 똑같은 정보를 가진 복제개체를 하나 더 만들어 저장하는 수밖에 없습니다. 스마트 포인터는 동적메모리를 관리하기 위한 객체임을 상기해야합니다.
make_shared 함수를 통해 복제된 객체를 저장하는 코드입니다. Myclass의 복제 생성자가 호출됩니다.
shared_ptr 객체는 스택메모리의 주소를 바로 담을 수는 없고, 담고자하는 항목의 복제 생성자를 호출한 뒤, 힙에 생성된 복제 객체의 주소를 담을 수는 있습니다. 조금 번거롭기는 하지만, 굳이 단점이라고 할 수는 없을 것 같습니다. 힙메모리를 관리하기 위한 객체에 스택주소를 담으려는 것은 스마트포인터 객체 사용이유를 무시하는 것이기 때문입니다. 추가적으로 힙 메모리도 보관하고 스택도 보관하는 객체 역시 유용하지 않습니다. 링크드 리스트가 힙메모리로만 이루어진 데이터가 아닌 스택과 힙이 섞여있다면, 노드 삭제 시 동적메모리인지 아닌지 확인하기 위한 절차가 추가로 필요할 것입니다. 그렇다면, 스택 주소를 담지 못하는 것은 큰 문제가 되지 않을 것 같습니다.
두 번째 단점은 여러개의 shared_ptr이 동일한 힙메모리를 보관한다면 레퍼런스 카운팅이 증가한다는 것입니다. 레퍼런스 카운팅으로 가장 마지막으로 사용한 곳에서 메로리를 해제해줄 수 있게 되었지만, 그것이 문제가 될 경우도 있습니다. 레퍼런스 카운팅은 올리고 싶지 않되, 그곳의 주소는 보관하고 싶을 때가 있습니다. 모든 shared_ptr 객체의 레퍼런스 카운팅을 1로 유지함으로써 메모리해제가 용이하도록 하고 싶을 경우가 해당됩니다. 이런 경우는 shared_ptr 객체의 get()메서드를 이용하여 힙메모리의 주소를 직접적으로 보관하거나, weak_ptr을 이용하면 레퍼런스 카운팅을 증가시키지 않고도 원하는 기능을 구현할 수 있습니다. 이동 메서드 구현을 통한 소유권 이전으로 레퍼런스 카운팅을 증가시키지 않는 방법도 있습니다.
Comments
Post a Comment