8 분 소요

1. 스마트 포인터 개념

  • 자동화된 자원관리 등의 기능을 제공하는 포인터
  • Raw Pointer
    • 생성자, 소멸자 등을 가질 수 없음
  • Smart Pointer
    • 생성자, 소멸자 등을 가짐
    • 생성/복사/대입/소멸의 과정에 추가적인 기능을 수행

1.1. 스마트 포인터의 원리

  • 내부적으로 객체의 주소를 보관하기 위해 포인터 멤버를 가짐
  • → 와 * 연산자를 재정의 되어 있음
#include <iostream>
#include <memory>
using namespace std;

class Car {
    int color;
public:
    ~Car() { cout << "~Car()" << endl; }
    void Go() { cout << "go()" << endl; }
};

int main()
{
    // Car* p = new Car;
    shared_ptr<Car> p( new Car );

    p->Go();    // p.operator->()
    (*p).Go();  // p.operator*()

    // delete p;
}

2. Shared_ptr

  • copy 초기화는 안되고 direct 초기화만 가능

2.1. shared_ptr의 메모리 구조

image

  • shared_ptr 생성 시, 참조 계수 등을 관리하는 control block이 생성됨
#include <iostream>
#include <memory>
using namespace std;

class Car {
    int color;
    int speed;
public:
    Car(int c=0, int s=0) : color(c), speed(s) {}
    ~Car() { cout << "~Car()" << endl; }
    void Go() { cout << "go()" << endl; }
};

int main()
{
    int a = 0; // copy initialization
    int a1(0); // direct initialization

    // shared_ptr<Car> p = new Car; // error
    shared_ptr<Car> p1(new Car); // ok

    shared_ptr<Car> p2 = p1;
}
  • 삭제자 변경
    • shared_ptr<> 생성자의 2번째 인자로 삭제자 전달
    • 함수, 함수 객체, 람다 표현식 등을 사용 가능
    • 할당자 변경은 3번째 인자로 가능
      #include <iostream>
      #include <memory>
      using namespace std;
        
      class Car {
          int color;
          int speed;
      public:
          Car(int c=0, int s=0) : color(c), speed(s) {}
          ~Car() { cout << "~Car()" << endl; }
          void Go() { cout << "go()" << endl; }
      };
        
      void foo(Car* p)
      {
          cout << "Delete Car" << endl;
          delete p;
      }
        
      int main()
      {
          // shared_ptr<Car> p(new Car);
          // shared_ptr<Car> p(new Car, foo);
          shared_ptr<Car> p(new Car,
                            [](Car* p){ delete p; } // deleter
                            // allocator
                            );    
      }
    

2.2. shared_ptr 과 배열

  • 삭제자를 변경해야 함
  • [] 연산자가 제공되지 않음
  • shared_ptr을 사용해서 배열을 관리하는 것은 권장되지 않음
  • C++17 이후
    • shared_ptr<T[]>를 사용
#include <iostream>
#include <memory>
using namespace std;

class Car {
    int color;
    int speed;
public:
    Car(int c=0, int s=0) : color(c), speed(s) {}
    ~Car() { cout << "~Car()" << endl; }
    void Go() { cout << "go()" << endl; }
};

int main()
{
    // shared_ptr<Car> p1(new Car[10]); // BUG - delete[], [] 연산 없음
    // shared_ptr<Car> p1(new Car[10], [](Car* p){ delete[] p; });
    shared_ptr<Car[]> p1(new Car[10]); // ok, C++17

    p1[0].Go();
}

2.3. 주요 멤버 함수

  • → 연산과 . 연산
    • → 연산 : 객체의 멤버에 접근
    • . 연산 : shared_ptr 자체 멤버에 접근
  • get : 대상체의 포인터 반환
  • use_count : 참조계수 반환
  • reset : 대상체 변경
  • swap : 대상체 교환
#include <iostream>
#include <memory>
using namespace std;

class Car {
    int color;
    int speed;
public:
    Car(int c=0, int s=0) : color(c), speed(s) {}
    ~Car() { cout << "~Car()" << endl; }
    void Go() { cout << "go()" << endl; }
};

int main()
{
    shared_ptr<Car> p1(new Car);

    p1->Go();

    Car* p = p1.get();
    cout << p << endl;

    shared_ptr<Car> p2 = p1;
    cout << p1.use_count() << endl; // 2

    // p1 = new Car; // error
    p1.reset(new Car); // ok
    p1.reset();

    p1.swap(p2);
}

2.4. make_shared

  • 메모리 조각화 현상을 해결하기 위해 나옴
  • 대상 객체와 제어 블럭을 동시에 메모리 할당하므로 효율적
  • 예외 상황에 좀 더 안전
  • 메모리 할당/해지 방식을 변경하려면 allocate_shared를 사용
#include <iostream>
#include <memory>
using namespace std;

class Car {
    int color;
    int speed;
public:
    Car(int c=0, int s=0) : color(c), speed(s) {}
    ~Car() { cout << "~Car()" << endl; }
    void Go() { cout << "go()" << endl; }
};

void* operator new( size_t sz) {
    cout << "new sz : " << sz << endl;
    return malloc(sz);
}

int main()
{
    // 메모리 조각화 현상
    // shared_ptr<Car> p1(new Car); // new sz = 8  : Car object
                                 // new sz = 24 : control block

    shared_ptr<Car> p1 = make_shared<Car>(1, 2); // new sz = 24 : Car obj + control block
    // shared_ptr<Car> p2(make_shared<Car>()); // 가능

    // 예외 상황에서 안전
    // 아래 3가지 일이 어떤 순서로 일어나는 지 표준에 언급되지 않음
    // 따라서 1번 후 3번으로 넘어가면 memory leak이 발생할 수 있음
    // f( shared_ptr<Car>(new Car), foo() );  // 1.메모리 할당
                                              // 2. shared_ptr 만듬
                                              // 3. foo 함수 호출

    // make_shared를 사용
    // f( make_shared<Car>(), foo() );

    // 메모리 할당/해지 변경하는 방식
    // shared_ptr<Car> p1 = allocate_shared<Car>(MyAlloc<Car>());
}

2.5. 주의사항

  • raw pointer를 사용해서 2개 이상의 shared_ptr를 생성하면 안됨

image

#include <iostream>
#include <memory>
using namespace std;

class Car {
    int color;
    int speed;
public:
    Car(int c=0, int s=0) : color(c), speed(s) {}
    ~Car() { cout << "~Car()" << endl; }
    void Go() { cout << "go()" << endl; }
};

void* operator new( size_t sz) {
    cout << "new sz : " << sz << endl;
    return malloc(sz);
}

int main()
{
    Car* p = new Car;

    shared_ptr<Car> sp1(p); // 제어 블럭 생성
    shared_ptr<Car> sp2(p); // 제어 블럭 생성, free 가 2번나서 에러

    // 아래와 같이 사용해야 됨, 만들면서 초기화
    shared_ptr<Car> sp3(new Car); // RAII
    shared_ptr<Car> sp3 = make_shared<Car>();
}

2.6. enable_shared_from_this

  • Worker의 객체 수명
    • 멀티스레드인 경우, 주 스레드의 sp1도 사용하지 않고, 새로운 스레드도 종료 되었을 때 파괴 되어야 함
    • Worker 객체가 자기 자신의 참조계수를 증가 해야 함

image

  • enable_shared_from_this 는 this를 가지고 제어 블럭을 공유하는 shared_ptr을 만들 수 있게 함
  • CRTP 기술
  • shared_from_this() 함수를 사용
  • shared_from_this()를 호출하기 전에 반드시 제어블럭이 생성되어 있어야 함
#include <iostream>
#include <memory>
#include <thread>
using namespace std;

class Car {
    int color;
    int speed;
public:
    Car(int c=0, int s=0) : color(c), speed(s) {}
    ~Car() { cout << "~Car()" << endl; }
    void Go() { cout << "go()" << endl; }
};

class Worker : public enable_shared_from_this<Worker> // CRTP
{
    Car c;
    shared_ptr<Worker> holdMe;
public:
    void Run()
    {
        holdMe = shared_from_this(); // 사용, 참조계수가 올라감

        thread t(&Worker::Main, this);
        t.detach();
    }
    void Main()
    {
        c.Go(); // 멤버 data(Car) 를 사용
        cout << "finish thread" << endl;

        holdMe.reset();
    }
};

int main()
{
    {
        shared_ptr<Worker> sp = make_shared<Worker>();
        sp->Run(); // 스레드 수행
    } // Worker가 파괴되어 Car가 없는 문제를
      // 해결하기 위해 enablle_shared_from_this 사용
}

3. weak_ptr

3.1. 상호 참조 문제

image

#include <iostream>
#include <memory>
using namespace std;

struct People
{
    string name;
    // shared_ptr<People> bf;
    People* bf; // Raw Pointer : 참조 계수가 증가하지 않음
                // 단점 : 대상 객체가 파괴되었는 지 알 수 없음
                // 솔루션 : weak_ptr
    // weak_ptr<People> bf;
    People(string s) : name(s) {}
    ~People() { cout << "~People :" << name << endl; }
};

int main()
{
    shared_ptr<People> p1( new People("KIM"));
    {
        shared_ptr<People> p2( new People("LEE"));

        // p1->bf = p2;
        // p2->bf = p1;
        p1->bf = p2.get();
        p2->bf = p1.get();
    } // p2 자원 파괴

    if (p1->bf != 0)
        cout << p1->bf->name << endl;
}

3.2. weak_ptr

  • use count가 증가하지 않는 스마트 포인터
  • expired() 멤버 함수로 대상 객체의 유효성을 확인할 수 있음
  • use_count가 0일 때 대상 객체는 파괴 되지만 제어 블록은 use_count, weak_count 모두 0일 때 파괴
#include <iostream>
#include <memory>
using namespace std;

struct People
{
    string name;
    weak_ptr<People> bf;
    People(string s="") : name(s) {}
    ~People() { cout << "~People :" << name << endl; }
    void Go() { cout << "Go" << endl; }
};

int main()
{
    // shared_ptr<People> wp; // use_count 증가
    weak_ptr<People> wp; // 증가 x

    // {
        shared_ptr<People> sp(new People);
        wp = sp;
        cout << sp.use_count() << endl;
    // } // 대상 객체 파괴

    if (wp.expired())
        cout << "destroy" << endl;
    else {
        cout << "exist" << endl;

        // weak_ptr을 사용해서는 대상객체에 접근할 수 없음
        // wp->Go(); // error

        // weak_ptr 을 다시 shared_ptr로 만들어야 함
        shared_ptr<People> sp2 = wp.lock();

        if( sp2 )
            sp2->Go();
    }
        
}
#include <iostream>
#include <memory>
using namespace std;

struct People
{
    string name;
    weak_ptr<People> bf;
    People(string s) : name(s) {}
    ~People() { cout << "~People :" << name << endl; }
};

int main()
{
    shared_ptr<People> p1( new People("KIM"));
    // {
    shared_ptr<People> p2( new People("LEE"));

    p1->bf = p2;
    p2->bf = p1;
    // } // p2 자원 파괴

    // if (p1->bf.expired()) {}

    shared_ptr<People> sp2 = p1->bf.lock();
    if (sp2)
        cout << sp2->name << endl;
    else
        cout << "destory" << endl;
}

4. unique_ptr

  • Raw Pointer와 동일한 크기를 가짐
  • 단, 삭제자 변경 시 크기가 커질 수 있음
#include <iostream>
#include <memory>
using namespace std;

struct People
{
    string name;
    weak_ptr<People> bf;
    People(string s="") : name(s) {}
    ~People() { cout << "~People :" << name << endl; }
    void Go() { cout << "Go" << endl; }
};

int main()
{
    shared_ptr<People> sp1(new People);
    shared_ptr<People> sp2 = sp1; // ok. use count 증가

    unique_ptr<People> up1(new People); // 자원 독점
    // unique_ptr<People> up2 = up1; // error
    cout << sizeof(sp1) << endl; // 16
    cout << sizeof(up1) << endl; // 8
}
  • 복사는 안되지만 이동은 가능
#include <iostream>
#include <memory>
using namespace std;

struct People
{
    string name;
    weak_ptr<People> bf;
    People(string s="") : name(s) {}
    ~People() { cout << "~People :" << name << endl; }
    void Go() { cout << "Go" << endl; }
};

int main()
{
    unique_ptr<People> up1(new People); // 자원 독점
    // unique_ptr<People> up2 = up1; // error

    unique_ptr<People> up2 = move(up1); // ok
}
  • 삭제자 변경 가능
#include <iostream>
#include <memory>
using namespace std;

struct Deleter
{
    void operator()(int* p) const
    {
        delete p;
    }
};

int main()
{
    // 1. 함수 객체 사용
    // unique_ptr<int, Deleter> up1(new int);

    // 2. 함수 포인터 사용
    // unique_ptr<int, void(*)(int*)> up1(new int, foo);

    // 3. 람다 표현식 사용
    auto f = [](int* p) { delete p; cout << "lambda" << endl; };

    unique_ptr<int, decltype(f)> up(new int, f);

    // 4. 배열을 관리하고 싶을 때, C++11 부터 가능
    unique_ptr<int[]]> up(new int[10]);
}

4.1. shared_ptr 과 unique_ptr 의 호환성

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    shared_ptr<int> sp(new int);
    unique_ptr<int> up(new int);
    
    shared_ptr<int> sp1 = up;       // error
    shared_ptr<int> sp2 = move(up); // ok

    unique_ptr<int> up1 = sp;       // error
    unique_ptr<int> up2 = move(sp); // error
}
#include <iostream>
#include <memory>
using namespace std;

class Shape {};
class Rect : public Shape {};
class Circle : public Shape {};

unique_ptr<Shape> CreateShape(int type)
{
    unique_ptr<Shape> p;
    switch(type)
    {
        case 1: p.reset(new Rect); break;
        case 2: p.reset(new Circle); break;
    }
    return p;
}

int main()
{
    // CreateShape는 unique_ptr로 만들어야
    // 받는 입장에서 unique/shared ptr 둘 다 사용 가능
    unique_ptr<Shape> p1 = CreateShape(1);
    shared_ptr<Shape> p2 = CreateShape(2);
}

참고

codenuri 강석민 강사 강의 내용기반으로 정리한 내용입니다.
코드누리


This is personal diary for study documents.
Please comment if I'm wrong or missing something else 😄. 

Top

댓글남기기