-
[C++] 스마트 포인터C++/이것이 C++이다 2021. 11. 3. 15:16
스마트 포인터는 포인터처럼 동작하는 클래스 템플릿 입니다.
동적 할당 된 변수를 자동으로 해제해주는 스마트 포인터는 메모리 관리에 큰 도움이 되는 기능입니다.
이번 글에서는 스마트 포인터의 4가지 종류에 대하여 살펴 볼 것입니다.
1. auto_ptr
더보기auto_ptr은 가장 오래 된 스마트 포인터 입니다.
가장 오래된 만큼 개선이 많이 이루어졌을 수도 있겠지만, 아쉽게도 auto_ptr은 그렇지 않습니다.
예제를 살펴보겠습니다.
#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "MyClass()\n"; } ~MyClass() { cout << "~MyClass()\n"; } }; int main() { cout << "======== Start ========\n"; { auto_ptr<MyClass> cls(new MyClass[5]); } cout << "========= End =========\n"; return 0; }
======== Start ======== MyClass() MyClass() MyClass() MyClass() MyClass() ~MyClass()
첫번째로, 배열 할당에서의 소멸자 호출 문제입니다.
배열로 동적 할당 했다면 반드시 배열로 삭제해야 합니다.
하지만 auto_ptr은 첫 번째 객체만 소멸시키고 런타임 에러까지 발생하는 것을 볼 수 있습니다.
다음 예제입니다.
#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "MyClass()\n"; } ~MyClass() { cout << "~MyClass()\n"; } void function() { cout << "function()\n"; } }; int main() { auto_ptr<MyClass> cls_a(new MyClass); auto_ptr<MyClass> cls_b; cout << cls_a.get() << "\n"; cls_b = cls_a; cout << cls_b.get() << "\n"; cout << cls_a.get() << "\n"; cls_a->function(); return 0; }
MyClass() 0062CE80 0062CE80 00000000
이번 예제 또한 정상적인 종료가 아닌 런타임 에러가 발생합니다.
마지막 출력을 보면, 두 포인터의 대입 연산 과정에서 cls_a가 NULL이 된 것을 볼 수 있습니다.
위와 같은 문제점 때문에 auto_ptr은 잘 사용하지 않습니다.
2. shared_ptr
더보기shared_ptr은 포인팅 횟수를 계산하는 포인터 입니다.
포인터가 소멸하더라도, 객체를 포인팅하는 다른 포인터가 남아있다면 객체가 소멸하지 않습니다.
예제를 살펴보겠습니다.
#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "MyClass()\n"; } ~MyClass() { cout << "~MyClass()\n"; } void function() { cout << "function()\n"; } }; int main() { cout << "======== Start ========\n"; shared_ptr<MyClass> cls_a(new MyClass); cout << cls_a.use_count() << "\n"; { shared_ptr<MyClass> cls_b; cls_b = cls_a; cout << cls_b.use_count() << "\n"; cls_b->function(); } cout << cls_a.use_count() << "\n"; cls_a->function(); cout << "========= End =========\n"; return 0; }
======== Start ======== MyClass() 1 2 function() 1 function() ========= End ========= ~MyClass()
use_count()메소드는 현재 객체를 포인팅하고 있는 포인터의 개수를 출력하는 메소드입니다.
출력을 확인해보면, 포인터의 갯수가 0이 되었을 때 소멸자를 호출하는 것을 알 수 있습니다.
또한, auto_ptr에서 발생했던 대입 연산 시 기존 포인터가 NULL이 되지 않는 것을 볼 수 있습니다.
shared_ptr은 배열 삭제 또한 지원합니다.
예제를 살펴보겠습니다.
#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "MyClass()\n"; } ~MyClass() { cout << "~MyClass()\n"; } }; void remover(MyClass* cls) { cout << "Remover()\n"; delete[] cls; } int main() { cout << "======== Start ========\n"; { shared_ptr<MyClass> cls_a(new MyClass[5], remover); } cout << "========= End =========\n"; return 0; }
======== Start ======== MyClass() MyClass() MyClass() MyClass() MyClass() Remover() ~MyClass() ~MyClass() ~MyClass() ~MyClass() ~MyClass() ========= End =========
shared_ptr 선언 시 함수를 등록하면 포인터의 소멸 시 호출됩니다.
위 함수는 배열 삭제 연산자가 있으므로, auto_ptr과 같은 배열 삭제에도 문제없이 대처할 수 있습니다.
shared_ptr은 auto_ptr보다 안정적이고, auto_ptr을 대체하여 사용할 수 있습니다.
auto_ptr의 문제점이 작은 편이 아니므로, shared_ptr을 사용함이 더 바람직하다고 생각합니다.
3. unique_ptr
더보기unique_ptr은 오로지 한 대상만 포인팅 할 수 있는 포인터 입니다.
예제를 살펴보겠습니다.
#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "MyClass()\n"; } ~MyClass() { cout << "~MyClass()\n"; } void function() { cout << "function()\n"; } }; int main() { cout << "======== Start ========\n"; { unique_ptr<MyClass> cls_a(new MyClass); unique_ptr<MyClass> cls_b(cls_a); // Line 15 } cout << "========= End =========\n"; return 0; }
E1776 : 함수 "std::unique_ptr<_Ty, _Dx>::unique_ptr(const std::unique_ptr<_Ty, _Dx> &)"을(를) 참조할 수 없습니다. 삭제된 함수입니다. C2280 : 'std::unique_ptr<MyClass,std::default_delete<MyClass>>::unique_ptr(const std::unique_ptr<MyClass,std::default_delete<MyClass>> &)': 삭제된 함수를 참조하려고 합니다.
위 예제는 Line 15에서 컴파일 에러가 발생하며 실행되지 않습니다.
발생하는 에러 코드와, 실제 unique_ptr의 소스코드를 보면 unique_ptr의 단일 참조 방법을 알 수 있습니다.
template <class _Ty, class _Dx /* = default_delete<_Ty> */> class unique_ptr { // non-copyable pointer to an object public: unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete; };
위 소스코드는 <memory> 의 unqiue_ptr 소스 코드의 일부입니다.
복사 생성자와 대입 연산자를 delete 한 것으로 unique_ptr은 문법적으로 다수의 포인터를 차단했습니다.
4. weak_ptr
더보기weak_ptr은 shared_ptr이 가리키는 대상을 참조하는 식으로 포인팅 할 수 있습니다.
shared_ptr과 다른점은, shared_ptr이 소멸자를 호출하는 시점을 판단하는 기준인 use_count에 포함되지 않는다는 것 입니다.
예제를 살펴보겠습니다.
#include <iostream> using namespace std; class MyClass { public: MyClass() { cout << "MyClass()\n"; } ~MyClass() { cout << "~MyClass()\n"; } void function() { cout << "function()\n"; } }; int main() { cout << "======== Start ========\n"; { shared_ptr<MyClass> cls_a(new MyClass); weak_ptr<MyClass> cls_b(cls_a); weak_ptr<MyClass> cls_c(cls_a); cout << cls_a.use_count() << "\n"; } cout << "========= End =========\n"; return 0; }
======== Start ======== MyClass() 1 ~MyClass() ========= End =========
예제에서 두 개의 weak_ptr을 생성하여 포인팅했지만, use_count는 변화하지 않았음을 볼 수 있습니다.
Visual Studio의 디버그 기능을 통해서 내부를 살펴보면 아래와 같습니다.
shared_ptr의 변수 중, _Uses와 _Weaks의 값이 다른것을 볼 수 있습니다.
weak_ptr을 함께 사용하는 카운터와, 그렇지 않은 카운터를 분리하여 shared_ptr의 소멸 시기를 결정합니다.
포인터는 메모리에 직접 접근할 수 있는, C와 C++의 가장 큰 특징 중 하나입니다.
매우 강력한 기능이지만, 그만큼 사용에 주의해야 하는 기능이기도 합니다.
메모리 누수와 같은 포인터의 잘못된 사용이 프로그램의 안정성에 큰 영향을 끼칠 수 있다는 것을 생각하면
메모리 해제를 자동으로 실행해주는 스마트 포인터는 포인터의 사용 부담을 크게 덜어주는 기능입니다.
다음 글은 예외 처리 입니다.
읽어주셔서 감사합니다.
'C++ > 이것이 C++이다' 카테고리의 다른 글
[C++] 함수 (0) 2021.11.28 [C++] 예외 처리 (0) 2021.11.25 [C++] 템플릿 (0) 2021.10.26 [C++] 객체 간 관계 (0) 2021.10.19 [C++] 다중 상속 (0) 2021.09.24