스마트 포인터

SOEUN CHOI·2022년 6월 24일
0

C++_study

목록 보기
12/15

씹어먹는 C++

14장 스마트 포인터 671p-704p


C++ 자원 관리

자원 해제 프로그래머가 직접 해제 필요

한번 받은 자원은 직접 해제해주지 않는 이상
프로그램이 종료되기 전 까지 영원히 남아있음
종료시에 운영체제가 알아서 해제

제대로 종료하지 않으면
포인터는 존재하지 않는데
heap에는 해당 객체가 남아있는 메모리 누수 발생

C++ 잘못된 방식 메모리 관리 할시의 문제
1) 메모리를 사용한 후에 해제하지 않은 경우

  • 시스템 메모리가 부족해져서 서버가 죽어버리는 상황이 발생
  • RAII를 통해 사용이 끝난 메모리는 항상 해제시켜 메모리 누수 문제 사전 방지 가능

2) 이미 해제된 메모리를 다시 참조하는 경우

  • double free 버그
    이미 소멸된 객체를 다시 소멸시켜서 발생하는 버그
  • 주로 만들어진 객체의 소유권이 명확하지 않아서 발생

스마트 포인터 (smart pointer)

RAII (Resource Acquisition Is Initialization)패턴

자원 (이 경우 메모리) 관리를 스택의 객체 (포인터 객체) 를 통해 수행

C++에서 두 가지 형태의 새로운 스마트 포인터를 제공

  • unique_ptr
  • shared_ptr

unique_ptr

특정 객체에 유일한 소유권을 부여하는 포인터 객체

  • -> 연산자를 오버로드
    포인터를 다루는 것과 같이 사용 가능
  • unique_ptr 은 어떤 객체의 유일한 소유권을 나타내는 포인터, unique_ptr 가 소멸될 때, 가리키던 객체 역시 소멸
  • 만약 다른 함수에서 unique_ptr 가 소유한 객체에 일시적으로 접근시, get 을 통해 해당 객체의 포인터를 전달.
  • 만약에 소유권을 이동을 원하면, unique_ptr 를 move

example code

#include <iostream>
#include <memory>

class A {
  int *data;
  public:
  A() {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
  }
  void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }
  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

//unique_ptr 사용
void do_something() {
  //A* pa = new A();와 같다
  std::unique_ptr<A> pa(new A());
  pa->some();
}

int main() { do_something(); }

자원을 획득함!
일반 포인터와 동일하게 사용가능!
자원을 해제함!

unique_ptr 복사 불가능

unique_ptr의 복사 생성자는 명시적으로 삭제되어 복사 불가능
어떠한 객체를 유일하게 소유하여야 하기 때문

example code

class A {
 public:
 A(int a){};
 A(const A& a) = delete; 
 /* 명시적으로 삭제하여 
 해당 함수를 사용할 수 없도록 함*/
};
int main() {
 A a(3); // 가능
 A b(a); // 불가능 (복사 생성자는 삭제됨)
}

unique_ptr 소유권 이전

unique_ptr 복사는 불가능하나 소유권 이전은 가능
즉 이동생성자는 가능함 move로 소유권을 이동시키는 것이므로

std::unique_ptr<A> pb = std::move(pa);
새로 생성된 pb이 소유권을 갖음
pa는 nullptr 즉 아무것도 가르키지 않음

  • 댕글링 포인터(dangling pointer)
    소유권이 이전된 unique_ptr을 재 참조 할 시 런타임 오류
    접근 하진 않도록 주의

unique_ptr 함수 인자로 전달

레퍼런스로 넘기면 소유의 권한에 위배
std::unique_ptr<A>& ptr 형태 < 올바르지 않음
원래의 포인터 주소값으로 넘김
pa.get() 형태로 넘기며(A* ptr)형태로 인자를 받으면 됨

unique_ptr 쉽게 생성

std::make_unique 함수 사용
템플릿 인자로 전달된 클래스의 생성자에 인자들에 직접 완벽한 전달 수행

example code

#include <iostream>
#include <memory>

class Foo {
  int a, b;
  public:
  Foo(int a, int b) : a(a), b(b) { std::cout << "생성자 호출!" << std::endl; }
  void print() { std::cout << "a : " << a << ", b : " << b << std::endl; }
  ~Foo() { std::cout << "소멸자 호출!" << std::endl; }
};

int main() {
  auto ptr = std::make_unique<Foo>(3, 5);
  //std::unique_ptr<Foo> ptr(new Foo(3, 5)); 불필요
  ptr->print();
}

생성자 호출!
a : 3, b : 5
소멸자 호출!

unique_ptr 를 원소로 가지는 컨테이너

복사 생성자가 없는 특성을 지님

  • vector push_back함수
    전달된 인자를 복사하여 집어 넣으므로 사용 불가능
    명시적으로 vector안으로 이동 > push_back의 우측값 레퍼런스 오버로딩하여 사용

    • emplace_back 함수
      vector 안에 unique_ptr 을 직접 생성하며 집어넣기 가능
      완벽한 전달(perfect forwarding) 을 통해,
      직접 unique_ptr<A> 의 생성자에 전달
      vector 맨 뒤에 unique_ptr<A> 객체를 생성
      즉, 불필요한 이동 과정을 생략 가능

      주의
      어떤 생성자로 emplace_back하는지 확인 할 것.
      요상한게 생성 될 수도 있다.
      int 1000인 원소를 원했으나 원소 1000개인 벡터를 추가하게 될 수 도...


      example code

      int main() {
          std::vector<std::unique_ptr<A>> vec;
      
          std::unique_ptr<A> pa(new A(1));
          //다음과 동일
          //vec.emplace_back(new A(1));
          vec.push_back(std::move(pa)); // 잘 실행됨
      }

shared_ptr

여러 개의 스마트 포인터가 하나의 객체를 같이 소유 해야 하는 경우

특정 자원을 몇 개의 객체에서 가리키는지를 추적한 다음에, 그 수가 0 이 되야만 비로소 해제를 시켜주는 방식의 포인터

example code

#include <iostream>
#include <memory>
#include <vector>
class A {
  int *data;
  public:
    A() {
      data = new int[100];
      std::cout << "자원을 획득함!" << std::endl;
    }
    ~A() {
      std::cout << "소멸자 호출!" << std::endl;
      delete[] data;
    }
};

int main() {
  std::vector<std::shared_ptr<A>> vec;
  
  /* 여러개의 ptr로 하나의 객체를 가르키는 것 가능
  아래 3개는 모두 같은 객체를 가르키는 shared_ptr*/
  
  vec.push_back(std::shared_ptr<A>(new A()));
  vec.push_back(std::shared_ptr<A>(vec[0]));
  vec.push_back(std::shared_ptr<A>(vec[1]));
  
  // 벡터의 첫번째 원소를 소멸 시킨다.
  std::cout << "첫 번째 소멸!" << std::endl;
  vec.erase(vec.begin());
  // 그 다음 원소를 소멸 시킨다.
  std::cout << "다음 원소 소멸!" << std::endl;
  vec.erase(vec.begin());
  // 마지막 원소 소멸
  std::cout << "마지막 원소 소멸!" << std::endl;
  vec.erase(vec.begin());
  std::cout << "프로그램 종료!" << std::endl;
}

자원을 획득함!
첫 번째 소멸!
다음 원소 소멸!
마지막 원소 소멸!
소멸자 호출!
프로그램 종료!

A객체의 erase가 ref count가 0이 되어야만 소멸자를 호출
즉 마지막 erase만 소멸자를 호출 가능

참조 개수 (reference count)

여러개의 소유권 가능 > ref count 증가
해당 참조 개수가 0이 되어야 해당 객체 해제 가능
모든 스마트 포인터 들이 소멸되어야만 객체 파괴 가능해짐

  • use_count 함수
    ref count 개수 확인
    p1.use_count();
  • 제어 블록 사용 하여 공유
    실제 객체를 가리키는 shared_ptr 가 제어 블록(control block) 을 동적으로 할당
    이후 shared_ptr 들이 이 제어 블록에 필요한 정보 공유하도록 구현
    복사 생성할 때 마다 해당 제어 블록의 위치만 공유

    shared_ptr에서
    생성 시 제어 블록의 참조 개수 ++
    소멸 시 제어 블록의 참조 개수 --하도록 작동

make_shared 로 생성

std::shared_ptr<A> p1(new A());> 바람직 하지 않음
해당 방법은 동적 할당을 2번 해야함

  • make_shared 함수
    아예 두 개 합친 크기로 한 번 할당 하는 것이 훨씬 빠름
    생성자의 인자들을 받아 객체 A 와 shared_ptr 의 제어블록 까지
    한 번에 동적 할당 후 생성한 shared_ptr 을 리턴
    std::shared_ptr<A> p1 = std::make_shared<A>();

shared_ptr 생성 시 주의

shared_ptr 은 인자로 주소값이 전달이 되면 해당 ptr이 첫번째가 되어버림
즉 주소값을 인자로 넘기면 각 ptr 제어블록 각각 생성

A* a = new A();
std::shared_ptr<A> pa1(a);
std::shared_ptr<A> pa2(a);


p1이 ref count 0으로 해당 A 객체를 삭제 하면
p2는 이미 해제된 객체를 가키며, 소멸 시키려 하여 오류가 발생

enable_shared_from_this

this 를 사용해서 shared_ptr 생성을 원하는 클래스가 있다면,
즉 객체 내부에서 자기 자신을 가리키는 shared_ptr 원하면

enable_shared_from_this 를 상속 받아 사용

  • enable_shared_from_this 클래스
    - shared_from_this 멤버 함수
    이미 정의되어 있는 제어 블록을 사용해서 shared_ptr 을 생성

    주의
    - shared_from_this는 제어 블록 확인만 함 생성은 안함
    즉, 해당 객체의 shared_ptr가 반드시 먼저 정의 되어야 함
    //불가능
    A* a = new A();
    std::shared_ptr<A> pa1 = a->get_shared_ptr();

example code

#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<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 shared_from_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;
}

자원을 획득함!
2
2
소멸자 호출!

서로 참조하는 shared_ptr

순환 참조
객체들을 더이상 사용하지 않아도 참조 개수가 절대로 0 이 될 수 없는 상황
shared_ptr 자체에 내재되어 있는 문제 > weak_ptr로 해결

weak_ptr

일반 포인터와 shared_ptr 사이에 위치한 스마트 포인터

스마트 포인터 처럼 객체를 안전하게 참조
shared_ptr 와는 다르게 참조 개수 늘리지 않음.

어떤 객체를 weak_ptr가 가리키더라도,
다른 shared_ptr를 사용하지 않으면 이미 메모리에서 소멸

weak_ptr 자체로는 원래 객체를 참조 불가능
반드시 shared_ptr 로 변환해서 사용
참조 하려는 객체가 이미 소멸되었다면 빈 shared_ptr 로 변환
아닐경우 해당 객체를 가리키는 shared_ptr 로 변환
-> lock 함수를 통해 수행

  • 제어 블록을 메모리에서 해제
    가리키는 weak_ptr 또한 0 개여야 가능
    따라서 제어 블록에는 참조 개수와 더불어 약한 참조 개수 (weak count) 기록

example code

#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; }
    //weak_ptr 사용
    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
soeun choi

0개의 댓글