RAII (Resource Acquisition is initialization) (자원 획득은 초기화와 같다)

자원의 안전한 사용을 위해 객체가 쓰이는 스코프를 벗어나면 자원을 해제해주는 패턴

heap에 할당된 자원은 명시적으로 delete 해주지 않으면 해제되지 않지만 stack에 할당된 자원은 범위를 벗어나면 메모리가 해제되며 소멸자가 불리는 원리를 이용한 것이다.

 

스마트 포인터(포인터 객체)의 종류 

1. unique_ptr 

C++에서 메모리를 잘못된 방식으로 관리했을 때 생기는 두 가지 문제 

 

1. 메모리 할당 후 해제하지 않아 메모리 누수가 생기는 경우

ㄴ RAII 패턴으로 해결 가능

 

2. 이미 해제한 메모리를 참조하는 경우

ㄴ 여러 개의 포인터가 같은 객체를 두번 해제하는 경우가 발생할 수 있다.

ㄴ 이미 해제한 객체를 참조할 가능성도 있다.

ㄴ unique_ptr로 해결 가능

 

// 함수 내에 정의(스택에 정의)된 객체이기 때문에 함수가 종료될 때 소멸자가 호출되고,
// 소멸자 안에서 자신이 가리키고 있는 자원 (A)를 해제해 준다.
std::unique_ptr<A> pa(new A());

// unique_ptr은 -> 연산자를 오버로드해서 포인터를 다루는 것처럼 함수를 호출할 수 있다.
pa->some();

 

unique_ptr 객체는 복사할 수 없지만, 소유권을 이전할 수 있다.

 

ㄴ unique_ptr의 복사 생성자는 delete 키워드로 명시(사용 금지) 하였기 때문에 하나의 자원(객체)를 여러 unique_ptr이 소유하지 않도록 한다.

 

std::unique_ptr<A> pb = std::move(pa);

ㄴ unique_ptr의 이동 생성자는 정의되어 있다. (자원의 주소를 단순 복사하는 형태로?)

ㄴ 이 문장이 실행된 이후 pa는 댕글링 포인터 (해제된 메모리 영역을 가리키는 포인터)가 되므로 참조해서는 안된다.

 

unique_ptr을 함수 인자로 전달하기

unique_ptr의 레퍼런스를 전달하는 경우

ㄴ unique_ptr의 의미가 퇴색된다. (컴파일 오류는 없으나 함수 안에서는 두 개의 이름(스마트 포인터)로 자원을 가리키기 때문)

 

대신에 자원의 포인터 주소값을 전달하자

ㄴ unique_ptr의 get() 함수는 자원의 포인터 주소를 반환하는데 이것을 함수에 전달하면 된다.

 

unique_ptr을 make_unique로 생성하기

C++14 부터는 make_unique를 사용하여 unique_ptr을 만들 수 있다.

// 기존 방법
std::unique_ptr<Foo> ptr(new Foo(3, 5));

// make_unique를 사용하는 방법
auto ptr = std::make_unique<Foo>(3, 5);

 

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

std::vector<std::unique_ptr<A>> vec;
std::unique_ptr<A> pa(new A(1));


// vector의 push_back에 pa를 넘겨주면 좌측값 레퍼런스로 인식하여 복사 생성자를 호출하기 때문에
// 컴파일 에러가 난다.
vec.push_back(pa); 

// 우측값 레퍼런스를 받도록 오버로딩된 push_back을 호출하기 위해 move()를 사용한다.
vec.push_back(std::move(pa));

// emplace_back을 사용하면 벡터 안에 unique_ptr을 생성과 동시에 집어넣을 수 있다.
// vec.push_back(std::unique_ptr<A>(new A(1))); 과 동일
vec.emplace_back(new A(1));

 

2. shared_ptr

ㄴ 하나의 자원을 여러개의 객체에서 사용하는 상황에서 자원을 해제 하기 위해서는 모든 객체를 소멸시키고 자원을 해제해야 한다. (= 여러 개의 스마트포인터 객체가 같은 객체를 가리킬 수 있다)

std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1);  // p2 역시 생성된 객체 A 를 가리킨다.

p1.use_count(); // 2
p2.use_count(); // 2

 

스마트 포인터의 참조 개수가 몇개 인지 어떻게 확인하는가?

ㄴ shared_ptr은 복사 생성될 때 제어 블록의 위치(참조 개수를 알고 있는 블록)을 공유한다.

 

첫 shared_ptr을 만들 때는 자원의 동적할당과 제어 블록의 동적할당이 필요하다.

 

shared_ptr을 make_shared로 생성하기

make_shared 함수는 자원과 제어 블록의 동적할당을 따로 두 번 하지 않고 한 번의 동적할당으로 처리한다.

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

 

shared_ptr 생성 시 주의 할 점

 

shared_ptr을 자원의 주소 값으로 생성하면 무조건 제어 블록을 생성한다. (첫 번째로 생성된 shared_ptr의 경우는 괜찮지만 두 번째 이상 부터는 두 개 이상의 제어 블록이 생기는 결과를 낳는다.) 첫 번째와 두 번째로 생성된 shared_ptr들 모두 제어 블록을 따로 가지며, 각자 자원에 대한 참조 개수가 1로 알고 있기 때문에 둘 중 하나의 shared_ptr이 소멸될 때 자원도 같이 소멸된다. (참조 개수가 0이 되면 자원도 소멸시키므로)

 

enable_shared_from_this

this를 사용해서 shared_ptr을 만들고 싶은 클래스가 있다면 std::enabled_shared_from_this를 상속받으면 된다.

class A : public std::enable_shared_from_this<A> {
  
  // 이미 정의되어있는 제어 블록을 사용해서 shared_ptr을 생성한다.
  // 즉 이미 이 객체에 대한 shared_ptr가 만들어져 있어야 한다.
  
  std::shared_ptr<A> get_shared_ptr() { return shared_from_this(); }
}

 

shared_ptr의 순환참조 문제와 3. weak_ptr

 

shared_ptr을 멤버로 갖는 객체를 서로 참조하고 있는 상태 : 순환 참조 상태

 

class A {
  std::string s;
  std::weak_ptr<A> other; // std::shared_ptr<A> other 순환 참조를 일으키는 shared_ptr 대신 사용
}

week_ptr은 이미 만들어진 shared_ptr나 week_ptr로부터만 생성될 수 있다. (생성자로 받는다.)

week_ptr은 참조 개수를 늘리지는 않는다.

week_ptr로 객체를 참조하기 위해서는 lock()함수를 사용하여 shared_ptr로 변환해야 한다.

week_ptr이 가리키고 있던 객체가 소멸되었다면 (shared_ptr이 가리키지 않아서) lock()은 빈 shared_ptr을 반환하고

가리키고 있던 객체가 아직 shared_ptr에 의해 가리켜지고 있다면 객체를 가리키는 shared_ptr을 반환한다.

 

객체는 해제되어도 제어 블록은 해제되면 안되는 경우

ㄴ 객체를 가리키는 shared_ptr은 0개이지만, week_ptr이 남아 있는경우

ㄴ 제어 블록을 해제하기 위해서는 week_ptr도 0개여야 한다.

 

제어 블록에는 참조 개수와 더불어 약한 참조 개수도 기록되어 있다.

 

참조:

https://modoocode.com/252

+ Recent posts