Chapter 3. 자원 관리

Effecive C++ 정리

Posted by Kyung Jun Cha on 2019-11-06

Chapter 3. 자원 관리


프로그래밍 분야에서 자원(resource)이란, 사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것을 일컫습니다.
ex) 동적 할당한 메모리, 파일 서술자, 뮤텍스 잠금, 그래픽 유저 인터페이스, 폰트, 브러시

항목 13. 자원 관리에는 객체가 그만


  • 예로 팩토리 함수를 통해 얻어낸 객체를 사용할 때 삭제를 책임져야 하는 쪽은 함수의 호출인데 이 과정에서 해제가 원할히 일어나지 못하는 경우가 있을 수 있습니다.
1
2
3
4
5
6
void f()
{
Investment* pInv = createInvestment(); //팩토리 함수 호출
... // pInv 사용, 사용 중에 return 되거나 에러 발생시 객체의 해제를 보장하지 못한다.
delete pInv; // 객체 해제
}
  • 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용합니다.

    1. 자원을 획득한 후에 자원 관리 객체에게 넘깁니다.
      RAII : Resource Acquisition is Initialization, 자원 획득은 즉 초기화입니다.
    2. 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 합니다.
  • 일반적으로 널리 쓰이는 RAII 클래스

    1. std::auto_ptr
      가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계되어있습니다.
      같은 객체를 가르키 auto_ptr이 둘 이상 존재하면 안 되기에 복사하면 원복 객체는 null로 만듭니다.
    1
    2
    3
    4
    std::auto_ptr<Investment> pInv1(createInvestment);

    std::auto_ptr<Investment> pInv2(pInv1) //pInv2가 현재 그 객체를 가르키고, pInv1은 null입니다.
    pInv1 = pInv2; // pInv1이 현재 그 객체를 가르키고, pInv2은 null 입니다.
    1. std::tr1::shared_ptr
      RCSP(reference-counting smart pointer) : 참조 카운팅 방식 스마트 포인터
      자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터입니다.
      복사 생성자, 대입 연산자에 정상적으로 동작합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void f()
    {
    ...
    std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1은 반환된 객체를 가르킵니다.

    std::tr1::shared_ptr<Investment> pInv2(pInv1); // pInv1, pInv2 모두 그 객체를 가르킵니다.

    pInv1 = pInv2; // 마찬가지입니다.
    } //pInv1, pInv2 가 소멸되어 가르키는 개수가 0이 되어 자원을 자동으로 삭제합니다.

항목 14. 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자.


  • RAII 객체의 복사는 관리하는 자원의 복사 문제를 안고 간다.
    ex) Mutex객체를 조작하는 경우 복사를 허용하면 안됩니다.
  • 복사 동작 구현 방법
    1. 복사를 금지합니다. (복사 연산을 private 멤버로 만듭니다.)
    2. 관리하고 있는 자원에 대해 참조 카운팅을 수행합니다. (std::tr1::shared_ptr을 사용합니다.)
    3. 관리하고 있는 자원을 진짜로 복사합니다. (깊은 복사(deep copy))
    4. 관리하고 있는 자원의 소유권을 옮깁니다. (auto_ptr의 복사 동작)

항목 15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자.


  • 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문입니다.
  • 변환 방법
    1. 명시적 변환(explicit conversion)
      멤버 함수를 사용해 객체에 들어 있는 실제 자원에 접근합니다.
      std::tr1::shared_ptrstd::auto_ptr은 get이라는 맴버 함수를 제공합니다. ex)pInv.get()
    2. 암시적 변환(implicit conversion)
      암시적 변환 함수를 제공하여 자원 접근을 매끄럽게 할 수 잇도록 합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Font {
    public:
    ...
    operator FontHandle() const
    {return f;}
    ...
    private:
    Fonthandle f; // 실제 폰트 자원
    };

안전성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면 암시적 변환이 괜찮습니다.

항목 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자.


  • new 표현식에 []를 썻으면, 대응되는 delete 표현식에도 []를 써야 합니다. 마찬가지로 new 표현식에 []를 안 썻으면, 대응되는 delete표현식에서도 []를 쓰지 말아야 합니다.

  • 메모리 배치구조

    1. 단일 객체
    Object
    1. 객체 배열
    n Object Object Object Object

    n은 배열의 크기

  • delete[]이 존재하면 n을 읽어 갯수만큼 소멸자를 호출하고 메모리를 해제한다.

배열 타입을 typedef 타입으로 만드는 경우 오류 발생의 소지가 있다.

1
2
3
4
5
typedef std::string AddressLines[4]; //배열 타입 선언
std::string *pal = new AddressLines; // 배열 선언

delete pal; //잘못된 delete
delete [] pal; //옳은 delete

항목 17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자.


1
2
3
4
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority()); //문제 발생 가능
  1. 매개 변수를 넘겨주는 연산 new Widget, priority(), tr1::shared_ptr 생성자 3가지가 실행됩니다.
  2. tr1::shared_ptr 생성자new Widget을 매개변수를 사용하기 때문에 순서가 뒤로 보장 되지만 다른 연산들과의 순서는 보장되지 못합니다.
  3. new Widget ➡️ priority() ➡️ tr1::shared_ptr 생성자 순서로 실행 되는 경우 priority() 호출 부분에서 예외가 발생하면 자원이 누출됩니다.
1
2
3
std::tr1::shared_ptr<Widget> pw(new Widget); //독립적인 문장으로 실행

processWidget(pw, priority());
  • 한 문장으로 만들지 않으면 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있습니다.