[Modern C++] 13-2. shared_ptr& weak_ptr

윤정민·2023년 7월 29일
0

C++

목록 보기
34/46

1. Shared_ptr

  • 특정 자원을 몇 개의 객체에서 가리키는지를 추적한 뒤, 그 수가 0이되면 비로소 해제시켜주는 방식의 포인터
  • 하나의 자원을 여러 객체에서 사용하는 경우 사용가능
  • 예시 코드
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1);  // p2 역시 생성된 객체 A 를 가리킨다.

1.1. reference count

  • shared_ptr내부에 참조 개수를 저장한다면 생기는 문제

    • 아래 코드의 경우 p1에 저장된 참조 개수를 건드릴 수 없음

      
      std::shared_ptr<A> p1(new A());
      std::shared_ptr<A> p2(p1);
      std::shared_ptr<A> p3(p2);

  • 위 문제를 해결하는 방법

    • shared_ptr가 제어블록을 동적으로 할당한 후, shared_ptr들이 이 제어 블록에 필요한 정보를 공유하는 방식으로 구현됨

1.2. make_shared를 사용한 생성

std::shared_ptr<A> p1(new A());

  • 위에서 사용한 생성 방법은 바람직하지 않음
    • A를 생성하기 위해서 동적 할당이 한 번 일어나야하고, 그 다음 shared_ptr의 제어 블록 역시 동적으로 할당해야 하기 때문
    • 두 개를 합친 크기로 한번에 할당하는게 빠름

std::shared_ptr<A> p1 = std::make_shared<A>();

  • make_shared함수는 A의 생성자의 인자들을 받아서 이를 통해 객체 A와 shared_ptr의 제어 블록까지 한 번에 동적할당 한 후에 만들어진 shared_ptr을 리턴

1.3. shared_ptr 생성 시 주의점

  • shared_ptr은 인자로 주소값이 전달되면, 마치 자기가 해당 객체를 첫번째로 소유하는 shared_ptr인 것 마냥 행동함

    • 따라서 인자로 주소 값을 사용하는 것은 지양
    • 예시 코드: 두 개의 제어 블록이 따로 생성됨
    A* a = new A();
    std::shared_ptr<A> pa1(a);
    std::shared_ptr<A> pa2(a);

  • 객체 내부에서 자기 자신을 가리키는 shared_ptr을 만들 때는 예외

    • 예시 코드: 컴파일 시 에러가 발생
    
    #include <iostream>
    #include <memory>
    
    class A {
      int *data;
    
     public:
      A() {
        data = new int[100];
        std::cout << "자원을 획득함!" << std::endl;
      }
    
      ~A() {
        std::cout << "소멸자 호출!" << std::endl;
        delete[] data;
      }
    
      std::shared_ptr<A> get_shared_ptr() { return std::shared_ptr<A>(this); }
    };
    
    int main() {
      std::shared_ptr<A> pa1 = std::make_shared<A>();
      std::shared_ptr<A> pa2 = pa1->get_shared_ptr();
    
      std::cout << pa1.use_count() << std::endl;
      std::cout << pa2.use_count() << std::endl;
    }
    • 자기 자신을 가리키는 shared_ptr가 있다는 사실을 모른채 새로운 제어 블록을 생성하기 때문
    • 이는 enable_shared_from_this를 통해 해결가능
    • 해당 객체는 shared_ptr이 반드시 먼저 정의되어 있어야만 가능
      std::shared_ptr<A> get_shared_ptr() { return shared_from_this(); }

2. weak_ptr

  • shared_ptr이 서로 참조하게 되면 참조개수가 절대로 0이 될 수 없음(순환 참조)
    • 이는 weak_ptr을 통해 해결 가능

  • 트리 구조를 지원하는 클래스를 만들 때 shared_ptr로 만든다면, 부모노드와 자식노드가 서로를 가리키기 때문에 순환참조가 발생
  • weak_ptr은 일반 포인터와 shared_ptr 사이에 위치한 스마트 포인터로, 스마트 포인터 처럼 객체를 안전하게 참조할 수 있게 해주지만, shared_ptr와는 다르게 참조 개수를 늘리진 않음
    • 어떤 객체를 weak_ptr가 가리키고 있더라도 다른 shared_ptr들이 가리키고 있지 않다면 메모리에서 소멸
    • 따라서 weak_ptr자체로는 원래 객체를 참조할 수 없고, 반드시 shared_ptr로 변환해 사용해야 함
      • 가리키고 있는 객체가 이미 소멸되었다면, 빈 shared_ptr로 변환
      • 아직 소멸되지 않았다면, 해당 객체를 가리키는 shared_ptr로 변환
  • 예시 코드
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class A {
  std::string s;
  std::weak_ptr<A> other;

 public:
  A(const std::string& s) : s(s) { std::cout << "자원을 획득함!" << std::endl; }

  ~A() { std::cout << "소멸자 호출!" << std::endl; }

  void set_other(std::weak_ptr<A> o) { other = o; }
  void access_other() {
    std::shared_ptr<A> o = other.lock();
    if (o) {
      std::cout << "접근 : " << o->name() << std::endl;
    } else {
      std::cout << "이미 소멸됨 ㅠ" << std::endl;
    }
  }
  std::string name() { return s; }
};

int main() {
  std::vector<std::shared_ptr<A>> vec;
  vec.push_back(std::make_shared<A>("자원 1"));
  vec.push_back(std::make_shared<A>("자원 2"));

  vec[0]->set_other(vec[1]);
  vec[1]->set_other(vec[0]);

  // pa 와 pb 의 ref count 는 그대로다.
  std::cout << "vec[0] ref count : " << vec[0].use_count() << std::endl;
  std::cout << "vec[1] ref count : " << vec[1].use_count() << std::endl;

  // weak_ptr 로 해당 객체 접근하기
  vec[0]->access_other();

  // 벡터 마지막 원소 제거 (vec[1] 소멸)
  vec.pop_back();
  vec[0]->access_other();  // 접근 실패!
}
  • 출력 결과
자원을 획득함!
자원을 획득함!
vec[0] ref count : 1
vec[1] ref count : 1
접근 : 자원 2
소멸자 호출!
이미 소멸됨 ㅠ
소멸자 호출!
profile
그냥 하자

0개의 댓글