Skip to main content

[Effective C++] Chapter 3. 자원 관리

이펙티브 C++ 책을 읽고 공부한 노트입니다.




Item 13: 자원 관리에는 객체가 그만! #

  • 프로그래밍 분야에서, 자원(resource) 이란?
    • 사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것이다.
    • 동적 할당 메모리, 파일 서술자(file descriptor), 뮤텍스 잠금(mutex lock), GUI에서 쓰이는 폰트나 브러시, 데이터베이스 연결, 네트워크 소켓 등

  • 어떻게 하면 팩토리 함수로 얻은 자원이 항상 해제되도록 할 수 있을까?
class Investment {};

Investment * createInvestment(); // 팩토리 함수 

void f()
{
  Investment * pInv = createInvestment(); // Investment를 생성한다.
  
  // ...
  
  // 도중하차되어 여기까지 코드가 도달하지 않으면 어떡할 것인가?
  delete pInv; 
}



자원 관리 객체 #

  • 객체를 써서 자원을 관리하는 게 최고다.
  • 자원을 객체에 넣음으로써 C++이 자동으로 호출해 주는 소멸자 덕분에 자원이 저절로 해제되도록 할 수 있다.
  • 자원 관리 객체의 사용 방법
    • (1) 자원을 획득한 후에 자원 관리 객체에게 넘긴다. (Resource Acquisition Is Initialization; RAII)
    • (2) 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.



스마트 포인터 #

  • 이런 객체 중에 하나가 바로 스마트 포인터(smart pointer) 이다.
    • auto_ptr은 블록을 벗어나면 소멸자가 자동으로 delete를 불러주도록 설계되어 있다.
void f()
{
  std::auto_ptr<Investment> pInv(createInvestment());
  // ...
} // auto_ptr이 소멸자를 통해 delete pInv를 한다. 
  • auto_ptr은 객체를 복사하면 원본 객체는 null로 바뀐다.
std::auto_ptr<Investment> pInv1(createInvestment());

// pInv2가 Investment를 가리키고, pInv1은 null이다.
std::auto_ptr<Investment> pInv2(pInv1);  

// pInv1이 Investment를 가리키고, pInv2는 null이다. 
pInv1 = pInv2; 

  • 참조 카운팅 방식 스마트 포인터(reference-counting smart pointer: RCDP)shared_ptr를 사용하면 여러 포인터가 객체를 가리키도록 할 수 있다.
    • shared_ptr은 가리키는 외부 객체의 수가 0이 되면 해당 자원을 자동으로 삭제한다.
std::shared_ptr<Investment> pInv1(createInvestment());

// pInv1와 pInv2 모두 Investment를 가리킨다. 
std::shared_ptr<Investment> pInv2(pInv1); 
pInv1 = pInv2; 

  • 스마트 포인터에서 주의할 점
    • delete를 부르는 것이지 delete []를 부르는 것이 아니다.
    • 따라서 아래와 같은 코드는 문제가 발생한다.
    • 배열을 위한 부스트(boost::scoped_array, boost::shared_array)는 Item 55를 참고하자.
std::auto_ptr<std::string> aps(new std::string[10]);

std::shared_ptr<int> apt(new int[1024]);



Item 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자 #

  • 모든 자원은 힙에 생기지 않으므로, delete를 알아서 해주는 스마트 포인터로는 다른 자원 관리가 불가능하다.
    • 그렇다면 직접 RAII 법칙에 맞는 자원 관리 클래스를 만들어볼 수 있겠다.
    • 예를 들어, 뮤텍스 잠금을 관리하는 클래스를 만든다고 하자.
class Lock
{
private:
  Mutex * mutexPtr;

public:
  explicit Lock(Mutex * pm) : mutexPtr(pm)
  {
    lock(mutexPtr); // 잠금
  }
  
  ~Lock()
  {
    unlock(mutexPtr); // 해제 
  }
};

// 사용자는 이렇게 사용하면 되겠다. 
int main()
{
  Mutex m;
  
  {
    Lock lock(&m); // 잠금을 건다. 
    // ... 임계 영역에서 연산을 수행한다. 
  } // 블록의 끝이므로 ~Lock()소멸자가 자동으로 불려서 잠금이 해제된다.  
}



자원 관리 객체의 복사 처리 #

  • 하지만 Lock 객체가 복사된다면 어떻게 해야 할까?
  • (1) 복사를 금지한다.
class Lock : private Uncopyable {}; // Item 6 참고 

  • (2) 참조 카운팅을 수행한다.
    • shared_ptr은 삭제자(delete) 지정을 허용한다. 그래서 소멸자에서 delete를 대신해 불릴 함수를 지정할 수 있다.
    • auto_ptr은 이런 기능이 없다.
class Lock
{
private:
  std::shared_ptr<Mutex> mutexPtr;
  
public:
  explicit Lock(Mutex * pm) 
    : mutexPtr(pm, unlock) // 삭제자를 unlock으로 지정한다. 
  {
    lock(mutexPtr.get()); // 잠금
  }
};

  • (3) 자원을 진짜로 복사한다.
    • 깊은 복사(deep copy)를 수행해야 하겠다.

  • (4) 자원의 소유권을 옮긴다.
    • auto_ptr의 복사 동작과 같다.



Item 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 #

  • 외부에서 자원 관리 클래스의 자원에 접근할 수 있도록 해야할 경우가 있다.
// Item 13에서 가져온 예제 

// 투자금이 유입된 이후로 경과한 날수를 계산한다. 
int daysHeld(const Investment * pi); 

void f()
{
  std::shared_ptr<Investment> pInv(createInvestment()); 
  
  int days = daysHeld(pInv); 
  // 이렇게 사용하고 싶지만 
  // pInv는 std::shared_ptr<Investment> 이고
  // 매개변수 형식은 Investment * 이다.
  // shared_ptr의 자원에 접근할 수 있어야 하겠다. 
} 



RAII 클래스를 자원으로 변환하는 방법 #

  • 명시적 혹은 암시적 변환 함수를 제공한다.
    • 어떤 함수를 사용할지는 사용 용도 및 환경에 따라 달라지겠다.
    • 늘 그렇지는 않지만 명시적 변환 함수를 제공하는 쪽이 나을 때가 많다.
    • RAII 클래스는 애초에 데이터 은닉이 목적이 아니므로, 자원을 공개한다고 해서 캡슐화에 위배되는 것은 아니다.

  • (1) 명시적 변환(explicit conversion)
    • shared_ptrauto_ptrget()이라는 멤버 함수를 제공한다. 이것으로 실제 자원을 가리키는 포인터의 사본을 얻을 수 있다.
int days = daysHeld(pInv.get()); // 동작한다. 

  • (2) 암시적 변환(implicit conversion)
    • shared_ptrauto_ptr은 포인터 역참조 연산자(operator->, operator*)를 오버로딩하고 있다.
class Investment
{
public:
  bool isTaxFree() const;
};

Investment * createInvestment();

int main()
{
  std::shared_ptr<Investment> pi1(createInvestment());
  bool isTaxFree1 = pi1->isTaxFree(); // operator->

  std::auto_ptr<Investment> pi2(createInvestment());
  bool isTaxFree2 = (*pi2).isTaxFree(); // operator*
}

  • 두 변환의 예시
class FontHandle {};

class Font
{
private:
    FontHandle f;
    
public:
  FontHandle get const // 명시적 변환 함수 
  {
    return f;
  }

  operator FontHandle() const // 암시적 변환 함수
  {
    return f;
  }
};

FontHandle getFont();
void changeFontSize(FontHandle f, int newSize);

int main()
{
  Font f(getFont());

  // (1) 명시적 변환
  changeFontSize(f.get(), 13);


  // (2) 암시적 변환 
  changeFontSize(f, 13); // Font 형식인 f가 FontHandler로 변환된다.
    
  // 원치 않는 경우가 발생할 수도 있다.
  FontHandle fh = f;
  // Font를 복사하고 싶었는데
  // f가 FontHandle로 바뀐 후 복사된다.
}



Item 16: newdelete를 사용할 때는 형태를 반드시 맞추자 #

  • new 연산자를 사용하면 벌어지는 일들
    • (1) 메모리가 할당된다. (operator new 함수가 사용된다)
    • (2) 생성자가 호출된다.
  • delete 연산자를 사용하면 벌어지는 일들
    • (1) 소멸자가 호출된다.
    • (2) 메모리가 해제된다. (operator delete 함수가 사용된다)

  • new를 사용해서 힙에 만들어진 단일 객체의 메모리 배치구조는, 객체 배열에 대한 메모리 배치구조와 다르다.
    • 힙의 맨 앞에 배열원소의 개수가 박혀 들어간다.
    • 이걸로 delete는 소멸자를 몇 번 부를지를 알 수 있다.

  • 따라서 new [] 표현식을 썼으면, delete [] 표현식을 맞춰 써야한다.
std::string * sPtr1 = new std::string;
std::string * sPtr2 = new std::string[100];

delete sPtr1;
delete [] sPtr2;

  • 왠만하면 배열 타입을 typedef 타입으로 만들지 말자.
    • delete []를 써야 하는지 헷갈린다.
    • vector 타입으로 만드는게 낫겠다.
typedef std::string AddressLines[4];

int main()
{
  std::string * pal = new AddressLines; // new string[4];
    
  delete pal;    // 이거 아니다.
  delete [] pal; // 이거 써야되는데 헷갈리지 않는가?
}



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

class Widget {};

int getPriority();

void processWidget(std::shared_ptr<Widget> pw, int priority);

int main()
{
  // 컴파일 에러 :
  // shared_ptr의 생성자는 explicit이라서 암시적 변환이 안 된다.
  processWidget(new Widget, getPriority());

  // 이것은 위험하다 :
  // new Widget으로 만들어진 자원이 shared_ptr에 저장되기 전, 그 사이에
  // getPriority()가 불리고 예외가 발생했다면
  // 만들어진 자원은 유실되고 만다.
  processWidget(std::shared_ptr<Widget>(new Widget), getPriority());
}

  • 따라서,new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 문장으로 만들어야 하겠다.
std::shared_ptr<Widget> pw(new Widget);

processWidget(pw, getPriority());