스마트 포인터

하루공부·2024년 1월 19일
0

C++

목록 보기
11/25
post-thumbnail

C++ 아이콘 제작자: Darius Dan - Flaticon


스마트 포인터

  • c++에선 직접 얻었던 자원은 직접 해제하지 않으면 프로그램이 끝날 때 까지 존재한다.

    자원 관리는 어렵다.
    사람인지라 깜빡할 수 있고 또 예외를 던지는 throw 아래에 delete가 있으면 예외 전달시 해제가 안됨.
    ==> 그래서 Resource Acquisition Is Initialization - RAII - 자원의 획득은 초기화다 패턴을 사용

    어떤 상황이 일어나도 할당된 자원들을 해제하는 패턴
    표준 라이브러리에 이를 사용하는 스마트 포인터가 있다.

  • 일반적인 포인터가 아닌 포인터 객체로 만들어 자신이 소멸될 때 가리키던 데이터도 같이 해제한다.
    ==> 객체를 통해 자원을 관리한다. 이 똑똑한 포인터가 스마트 포인터.
    3가지의 스마트 포인터가 있다.
    1) unique_ptr
    2) shared_ptr
    3) weak_ptr


unique_ptr

  • 잘못된 자원 관리로 이미 해제된 메모리를 다시 참조하는 경우가 있다

    이미 소멸된 객체를 다시 소멸시켜 에러를 발생 ==> double free 버그 라고 한다.

    위와 같은 문제가 발생한 이유는 만들어진 객체의 소유권이 명확하지 않아서다.

    • 어떤 포인터에 객체의 유일한 소유권을 부여해서, 이 포인터 말고는 객체를 소멸시킬 수 없다! 라고 한다면, 위와 같은 문제가 발생하지 않을 것이다.

  • C++ 에 특정 객체에 유일한 소유권을 부여하는 포인터 객체를 unique_pt가 있다.
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;
}
};                                                     
void do_something() {                                    
std::unique_ptr<A> pa(new A());  // A* pa = new A();와 똑같은 문장이라 생각하면 편하다.                     
pa->some();   // 생성된 스마트 포인터를 실제 포인터 처럼 활용한다.
}
int main() { do_something(); }
  • 위 예제처럼 <>에 포인터가 가리킬 클래스를 전달하고 가리킬 객체를 ()에 전달하여 정의한다.

  • unique_ptr로 인해서 RAII패턴을 구현할 수 있다.

    해당 스마트 포인터는 스택에 정의된 객체이기 때문에 해당 객체의 범위가 종료되면 소멸자가 자동으로 호출된다.


  • 스마트 포인터 객체를 가리킬 수 있는가?

    std::unique_ptr<\A> pb = pa; ==> 컴파일 오류가난다. ==> 삭제된 함수를 사용했기 때문에다.

    unique_ptr은 어떠한 객체를 유일하게 소유해야 하기에 복사 생성자가 명시적으로 삭제됨.

    삭제된 함수?? A(const A& a) = delete; 처럼
    = delete;를 사용하면 명시적으로 함수를 사용못하게 막는다.
    사용하면 컴파일 오류가 발생


unique_ptr 소유권 이전하기

  • 앞서 unique_ptr 는 복사가 되지 않는다고 하였지만 소유권을 바꿀 수 있다.
    std::unique_ptr<A> pb = std::move(pa); // pb 에 소유권을 이전.

    이는 이동 생성자를 사용한 것으로 이전되면 아무것도 가리키지 않고 nullptr이 들어간다
    해당 포인터를 댕글링 포인터라고 한다.


unique_ptr를 함수 인자로 전달하기

해당 포인터는 어떠한 객체의 소유권을 의미하는 것인데
함수에 인자로 전달한다면 함수 내부에서 unique_ptr은 더이상 유일한 소유권이라고 보기 어렵다.

해당 스마트 포인터의 의미가 사라진다. 또한 규칙 위반이다.

  • 그럼 어떻게 사용해야 하는가?
    ==> 해당 스마트 포인터가 가리키는 객체의 주소값을 전달해주면 된다.
    • unique_ptr의 get함수는 실제 객체의 주소값을 리턴한다.
     void do_something(A* ptr) { 무언가를 한다... }  ==> 일반적인 포인터를 인자로 받는다
     std::unique_ptr<\A> pa(new A());
     do_something(pa.get()); }  ==> 클래스 A의 객체의 주소를 넘겨줌

std::make_unique

  • C++ 14 부터 unique_ptr 을 간단히 만들 수 있는 std::make_unique 함수를 제공
    ex) auto ptr = std::make_unique<A>(1, 2);
    • make_unique 함수는 템플릿 인자로 전달된 클래스의 생성자에 인자들에 직접 완벽한 전달
    • 따라서 기존 처럼 불필요하게 std::unique_ptr<\A> ptr(new A(1, 2 )); 할 필요가 없다

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

  • 그냥 unique_ptr을 원소로 하는 vector에 해당 스마트 포인터를 push_back으로 넣으면 컴파일 오류가 남.

    vector의 push_back함수는 전달 받은 인자를 복사해서 집어 넣는 것임.

    그렇기에 unique_str을 vectre안으로 이동시켜줘야 한다. vec.push_back(std::move(pa));

  • 그런데 emplace_back을 사용하면 vector안에 unique_str을 직접 생성하면서 집어넣을 수 있다.

    emplace_back 함수는 전달된 인자를 완벽한 전달로 직접 unique_ptr<\A>의 생성자에 전달 해서 vector 맨 뒤에 unique_ptr<\A> 객체를 생성
    ==> 따라서, 위에서 처럼 불필요한 이동 연산이 필요 없다.



shared_ptr

  • 동인한 객체를 공유해서 해당 스마트 포인트의 여러개로 가리킬 수 있다.
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1); // p2 역시 생성된 객체 A 를 가리킨다.

  • 해당 스마트 포인터는 참조 개수(reference count)라는게 있는데 해당 포인터가 객체를 가리키는 개수이다.

    참조 개수가 0이 되어야 가리키던 객체를 해제할 수 있다.
    use_count로 현재 참조 개수가 몇개인지 확인할 수 있다. ex) std::cout << p.use_count();


  • 여기서 해당 포인터들은 어떻게 참조 개수를 서로 공유하는가?

    처음으로 실제 객체를 가리키는 shared_ptr이 제어블럭(control block)을 동적으로 할당한다.
    이 제어 블럭에서 서로 정보를 공유한다. ==> 참조 개수를 공유
    해당 포인터를 생성할 때 마다 해당 제어 블럭의 위치를 공유하기만 하면 끝


std::make_shared

  • 해당 포인터를 다음과 같이 평범하게 생성할 것이다.
    std::shared_ptr<A> p1(new A());
    ==> 이 방법은 A를 생성하기 위해 동적 할당 1번, 제어 블럭 생성을 위해 동적 할당 1번 ==> 총 2번이다.

    동적 할당을 2번할 것을 알고 있으니 그냥 처음부터 2개를 합친 크기로 1번 할당하는게 훨씬 빠르다.


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

<>에 sharedptr이 가리킬 타입을 전달하면 알아서 해당 타입의 생성자와 제어 블럭까지 1번에 할당한다.
()에는 해당 타입 생성자의 인자를 전달하면 된다.

해당 함수는 해당 타입의 생성자한테 완벽한 전달을 수행한다.


  • shared_ptr 생성시 주의할 점

    해당 포인터를 생성할 때 인자로 객체가 아닌 주소값을 전달하면
    해당 객체를 첫번째로 소유하는 shared_str마냥 행동한다.

    그러면 여러개의 제어 블럭이 생기고 참조 개수가 서로 공유되지 않아
    아직 스마트 포인터가 남아있음에도 가리키는 객체를 해제할 수 도 있다.


enable_shared_from_this

  • 위에서 주소값을 넘기지 못한다고 했는데 객체 내부에서 자기 자신을 리턴할 때는 우짜지?
    ==> 보통 this를 넘기는데 불가능하다.

    this 를 사용해서 shared_ptr 을 만들고 싶은 클래스가 있다면
    enable_shared_from_this 를 상속 받으면 hared_from_this(); 함수를 사용할 수 있다.

    이 함수는 이미 정의되어 있는 제어 블럭을 사용해서 shared_ptr을 생성한다.
    this를 사용할 자리에 그냥 해당 함수를 호출하면 된다.

    1가지 주의사항은 해당 함수를 사용하기 위해서는 반드시 해당 객체에 대한 shared_ptr 1개가 이미 있어야 한다.
    ==> 제어 블럭을 확인만 할 뿐 새로운 제어 블럭을 만들지 않기 때문이다.



weak_ptr

  • 해당 스마트 포인터는 shared_ptr의 순환 참조 문제를 해결하기 위해 존재한다.
class A {
int *data;
std::shared_ptr<A> other;
public:
A() {
data = new int[100];
std::cout << "자원을 획득함!" << std::endl; }
~A() {
std::cout << "소멸자 호출!" << std::endl;
delete[] data; }
void set_other(std::shared_ptr<A> o) { other = o; } };
int main() {
std::shared_ptr<A> pa = std::make_shared<A>();
std::shared_ptr<A> pb = std::make_shared<A>();
pa->set_other(pb);
pb->set_other(pa); }  ==> 소멸자가 실행되지 않음

객체 1이 파괴 되기 위해서는 객체 1을 가리키고 있는 shared_ptr의 참조개수가 0이 되어야 하고
객체 2가 파괴 되기 위해서는 반대이다 => 이러지도 저러지도 못하는 상황이 된다.
==> 이 때 weak_ptr을 사용한다.


  • 해당 포인터는 shared_ptr과 비슷하지만 참조 개수를 늘리지 않고 제어 블럭을 공유한다.
    • shared_ptr을 사용하면서 참조 카운트에 영향을 받지 않는 스마트 포인터가 필요할 때 사용한다.
    • weak_ptr을 사용하면 shared_ptr가 관리하는 자원(메모리)을 참조카운트에 영향을 미치지 않으면서
      참조 타입으로 가질 수 있습니다. => 자원을 할당 받아도 잠조 카운팅에 영향을 미치지 않는다

  • weak_ptr이 자원에 대한 참조를 받으려면 shared_ptr이나 다른 weak_ptr의 자원을 복사 생성자나 대입 연산자를 통해서 할당 받을 수 있다.
auto ap = std::make_shared<A>();
std::weak_ptr<A> ap1(ap); // shared_ptr을 weak_ptr의 복사생성자로 weak_ptr(ap1) 객체 생성
std::weak_ptr<A> ap2 = ap1;  // weak_ptr을 weak_ptr의 대입연사자로 weak_ptr(ap2) 객체 생성
  • 만약 할당 받은 자원의 shared_ptr의 참조 카운트가 0이 되어 객체가 해제 된다면 더 이상 weak_ptr을 사용할 수 없고 비어 있는 상태가 되면 제어 블럭의 주소를 가지지 않는다.

    weak객체에 접근할 수 있다고(weak가 가리키고 있다고) 해서 해당 자원에 접근할 수 있다는 것을 보장할 수 없기 때문에
    weak_ptr은 할당 받은 자원을 직접적으로 사용하지 못한다
    ==> 그냥 빈 곳을 가리킬 수 있어서

    그래서 직접적인 사용을 막기 위해 해당 포인터의 함수에 ->, *, get() 연산자는 없다.


  • 그럼 어떻게 사용하는가??

    lock() 함수를 통해 해당 포인터 객체로 shared_ptr 객체를 만들어 사용한다.
    std::shared_ptr<A> o = weak1.lock();


  • 마지막으로 shared_ptr과 weak_ptr은 control block을 공유한다 했는데
    shared_ptr의 참조 개수가 0이 되고 weak_ptr이 남아 있으면 해당 제어 블럭은 어떻게 될까??

    제어 블럭을 메모리에서 해제하기 위해서는 이를 가르키는 weak_ptr 역시 0개 여야한다
    그래서 해당 포인터의 개수를 파악하기 위해 제어 블럭에 참조 개수와 함께
    약한 참조 개수(weak count)를 기록한다.


공부한 내용 복습

개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

0개의 댓글