Programming Language/C++

[C++] 스마트 포인터

lumana 2024. 6. 28. 23:30

스마트 포인터

  • 스마트 포인터를 이용하면 동적할당된 메모리를 알아서 지워준다

  • 3가지 종류 존재

    • unique_ptr

    • shared_ptr

    • weak_ptr

unique_ptr

  • int *a = new int(5);를 스마트 포인터를 이용하여 동적할당해보자
#include <iostream>
#include <memory>

using namespace std;

int main() {
        // int *a = new int(5);를 스마트 포인터를 이용하여 생성해보자
    // 다음은 불가능
    // unique_ptr<int> a = new int(5);
    unique_ptr<int> a(new int(5));
}
  • unique_ptr a = new int(5);은 불가능함

    • 이 경우 변환 생성자가 호출되는데, 스마트 포인터의 경우 묵시적 형변환이 불가능하기 때문
  • 따라서 생성자를 이용해야 한다

    • unique_ptr a(new int(5));
  • a가 scope(위 예시에서는 메인함수)를 벗어나면 a가 사라지면서 a가 가리키는 객체가 사라진다

  • 생성자와 소멸자를 통해서 작동 과정을 실제로 확인해보자

#include <iostream>
#include <memory>

using namespace std;

class Test {
    public:
    Test() { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
};

int main() {
    cout << "main 함수 시작" << endl;
    unique_ptr<Test> a(new Test);
    cout << "main 함수 종료" << endl;
}
  • delete를 적어주지 않았는데도 스마트 포인터가 객체를 알아서 삭제해준다

왜 unique일까?

  • 여러 개의 포인터가 같은 객체를 가리킬 수 없게 막아뒀기 때문

    • a, b, c라는 포인터가 있을 때, a, b, c는 같은 객체를 가리킬 수 없다
#include <iostream>
#include <memory>

using namespace std;

class Test {
    public:
    Test() { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
};

int main() {
    cout << "main 함수 시작" << endl;
    unique_ptr<Test> a(new Test);
    /* 다음은 불가능
    // unique_ptr<Test> b(a);
    // b = a
    */
    cout << "main 함수 종료" << endl;
}
  • b라는 포인터가 a가 가리키는 객체를 가리키려고 하고 있기 때문에 오류가 발생한다

    • unique_ptr b(a); 그리고 b = a;는 불가능하다

메모리 소유권

#include <iostream>
#include <memory>

using namespace std;

class Test {
    public:
    Test() { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
};

int main() {
    int *a = new int(5);
    int *b = a; // 얕은 복사

    cout << *b << endl;
    delete a;
    // 한 번 더 해제하면 안된다
    // delete b;
}
  • a와 b 중 객체를 삭제할 의무를 가진게 메모리를 해제해줘야 한다

    • 이런 것을 메모리 소유권이라고 한다

    • 위 예시에서는 a가 객체의 메모리 관리를 전담해서 하고 있다.

    • b는 메모리 소유권을 가지고 있지 않다

int main() {
    int *b;
    if (...) {
        int *a = new int(5);
        /// ...
        b = a; // 소유권 이전
    }
    cout << *b << endl;
    // delete a를 할 수가 없다.
    // 메모리 소유권이 b로 이전되어 b에서 해제해준다
    delete b;
}
  • if문이 끝나면 a 포인터가 사라지기 때문에 5의 값을 갖는 객체를 접근할 수 있는 포인터가 b밖에 없다.

    • 따라서 b를 통해서만 delete를 할 수 있다.
    • b = a;에서 메모리 소유권을 이전해준다
int main() {
    int *a = new int(5);
    int *b = a; // 소유권 이전이라고 "해석"

    cout << *b << endl;
        delete b
}
  • 아까와 동일한 코드. 위에서는 delete a;를 해줬다

  • 여기서는 delete b로 해제하고 있다.

    • 코드는 똑같은데 위에서는 a로 해제하고, 여기서는 b로 해제하고 있다.

    • 동일한 코드인데 메모리 소유권이 다를 수가 있나?

      • 소유권이라는 개념은 의미론적인 개념이다. 문법적으로 소유권이 넘어갔다는 것을 알 수 있는 방법은 없음

      • int *b = a; 에서 소유권 이전이라고 "해석" 할 뿐이다 (semantic. 의미를 해석)

  • 스마트 포인터를 사용하면 소유권을 문법적인 차원에서 명시적으로 드러나게 할 수 있다

int main() {
    unique_ptr<int> a(new int(5));
    // b에게 소유권을 이전하고 싶으면 a가 5를 갖는 객체와 연결을 끊어줘야 한다
    unique_ptr<int> b(a.release()); // 소유권 이전
    // release() : a가 가지고 있는 소유권을 포기하고 다른 포인터로 넘겨주겠다.
    cout << *b << endl;
}
  • unique_ptr이기 때문에 b에게 소유권을 이전하고 싶으면 a가 5를 갖는 객체와 연결을 끊어줘야 한다

    • unique_ptr b(a.release()); 에서 소유권 이전

    • release() : a가 가지고 있는 소유권을 포기하고 다른 포인터로 넘겨주겠다

  • 그리고 소유권 이전을 안하고 참조를 복사할 수 있는 방법은 없다.

    • 소유권 이전 안하고 unique_ptr b(a); 이런 것은 불가능

reset()

int main() {
    unique_ptr<int> a(new int(5));
    a.reset(new int(6));
}
  • reset()은 재설정을 한다는 의미이다.

    • a가 가리키는 객체를 다른 객체로 바꾼다는 것
  • 클래스의 생성자와 소멸자를 통해서 동작을 확인해보자

#include <iostream>
#include <memory>

using namespace std;

class Test {
   public:
    Test(int x) : x(x) { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
    int GetX() const { return x; }

   private:
    int x;
};

int main() {
    unique_ptr<Test> a(new Test(5));
    cout << a->GetX() << endl;
    cout << "====" << endl;
    a.reset(new Test(6));
    cout << a->GetX() << endl;
    cout << "====" << endl;
}
  • 실행 결과
생성자
5
====
생성자
소멸자
6
===
소멸자
  • new Test(6) 객체가 먼저 생성되고 Test(5) 객체가 삭제된다

shared_ptr

  • 여러 포인터가 한 객체를 공유할 수 있다
#include <iostream>
#include <memory>

using namespace std;

class Test {
   public:
    Test(int x) : x(x) { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
    int GetX() const { return x; }

   private:
    int x;
};

int main() {
    // 방식 1
    shared_ptr<Test> a = make_shared<Test>(5);
    // 방식 2
    shared_ptr<Test> a(new Test(5));

    shared_ptr<Test> b = a;

}
  • shared_ptr를 만드는 방식에는 두 가지 방법이 있다.

    • make_shared 이용

      • shared_ptr a = make_shared(5);
    • 생성자 이용

      • shared_ptr a(new Test(5));
  • shared_ptr는 복사 생성, 복사 대입 모두 가능하다

    • shared_ptr b = a;

    • shared_ptr b(a);

    • 여러 포인터가 한 객체를 가리킬 수 있게 된다

  • shared_ptr은 use-count라는 특별한 값을 저장하여, 이 객체를 몇 개의 포인터가 가리키고 있는지 관리한다

    • a, b가 같은 객체 Test(5)를 가리키는 경우 use-count의 값은 2

    • a가 사라진 상황에서는 use-count의 값은 1

    • b가 사라진 상황에서 use-count는 0

  • b.use_count()을 통해서 실제 use-count 값을 확인해볼 수도 있다

int main() {
    shared_ptr<Test> a(new Test(5));
    {
        shared_ptr<Test> b = a;
        cout << a.use_count() << endl;
        cout << b.use_count() << endl;
    }
    cout << a.use_count() << endl;
}   
  • 실행 결과
생성자
2
2
1
소멸자

weak_ptr

  • weak, 약하다: 소유권은 없지만 어떤 객체를 가리키는 것은 할 수 있다.

  • a가 가리키는 Test(5)를 weak 포인터 b가 가리킬 때

    • a가 삭제되면 use_count는 0이 된다
#include <iostream>
#include <memory>
using namespace std;

class Test {
   public:
    Test(int x) : x(x) { cout << "생성자" << endl; }
    ~Test() { cout << "소멸자" << endl; }
    int GetX() const { return x; }

   private:
    int x;
};

int main() {
    weak_ptr<Test> b;
    {
        shared_ptr<Test> a(new Test(5));
        b = a;
        cout << a.use_count() << endl;
        cout << b.use_count() << endl;
    }
    cout << b.use_count() << endl;
        // 다음은 불가능
        // cout << b->GetX() << endl;
}   
  • 실행 결과
생성자
1
1
소멸자
0
  • 마지막 줄 cout << b.use_count() << endl;에서

    • b는 이미 소멸된 객체를 가리키고 있다.

    • cout << b->GetX() << endl; 이런 것은 불가능한다

    • b는 가리키는 객체가 소멸된 Dangling pointer 이기 때문이다

  • 대신에 b가 가리키는 객체가 존재하는지 확인할 수 있는 방법이 존재한다

int main() {
    weak_ptr<Test> b;
    {
        shared_ptr<Test> a(new Test(5));
        b = a;
        cout << a.use_count() << endl;
        cout << b.use_count() << endl;
    }
    cout << b.use_count() << endl;
    if (!b.expired()) {
        cout << b.lock()->GetX() << endl;
    }
}   
  • b가 가리키는 객체가 소멸되지 않았다면 b.lock()에서 shared_ptr를 받아와서 GetX()를 호출할 수 있다.

  • 반대로 만약 b가 가리키는 대상이 이미 소멸했다면 b.lock()은 널 포인터를 리턴한다

참조) 두들낙서 C/C++ 강좌