ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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

    댓글

Designed by Tistory.