항목 13 자원 관리에는 객체가 그만!
항목 12는 복사 생성자를 만들때 파생 객체의 복사 생성자에서 부모 클래스의 복사 생성자를 호출하여 빠짐없이 복사하자는 내용이었는데 크게 다룰 내용이 없어서 건너 뛰었다. 이번 항목은 자원관리에 객체를 사용하자는 내용인데 우선 아래 코드를 통해 몇가지 가정을 하도록 하자.
class Investment { ... }; // 대충 투자 관련 처리를 담당하는 기본 클래스
Investment* createInvestment(); // Investment클래스에서 파생된 객체를 동적할당하여 포인터를 반환하는 팩토리 함수
자 그리고 이 클래스와 함수를 아래 코드처럼 사용한다고 치자.
void f()
{
Investment* pInv = createInvestment(); // 팩토리 함수 호출
... // pInv 사용
delete pInv; // 객체 해제
}
자 그런데 우리가 한 번이라도 delete를 사용해 객체를 해제 하는것을 잊지 않을 수 있을까? 그래 우리가 백 번 고민해서 그러한 일이 없다고 치자. 그렇다면 이 함수 내부에서 delete까지 안전하게 예외 없이 잘 온다는 보장이 있나? 혹여나 이 중간에 예외가 발생하여 함수 밖으로 나가지면 메모리 누수가 발생하게 된다. 우리는 그래서 이를 해결하기위해 auto_ptr이라는 스마트 포인터를 사용하게 된다. (요즘은 auto_ptr 거의 안씀!)
void f()
{
std::auto_ptr<Investment> pInv = createInvestment(); // 팩토리 함수 호출
... // pInv 사용
}
이 auto_ptr은 소멸자에서 메모리를 자동으로 해제 하도록 되어 있기 때문에 혹시나 예외가 발생에 함수 실행 중간에 튕기더라도 메모리 누수가 발생 하지 않는다. 하지만 여기서 auto_ptr은 단점이 있다. 오로지 소유권을 한 객체만 가지도록 한다는 것이다.
std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1); // pInv1에서 pInv2로 복사가 일어남, pInv1은 null이됨
pInv1 = pInv2; // pInv2의 값이 pInv1으로 복사됨, pInv1은 원래 값을, pInv2는 null을 가리킴
자 위의 코드와 같이 auto_ptr을 사용하면 복사를 시키면 원본이 null이 되어버리는 일이 발생한다. 그래서 이를 위한 대안으로 참조 카운팅 방식 스마트 포인터(RCSP)를 쓰면 아주 좋다. RCSP는 어떤 자원을 가리키는 외부 객체 개수를 세고 있다가 그 갯수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터이다. 이 책에서는 TR1에서 제공하는 std::tr1::shared_ptr에 대해 설명하지만 지금은 그냥 std::shared_ptr임을 유의하자. std::shared_ptr은 대표적인 RCSP인데 이것을 이용해 아까 f함수를 작성하면 다음과 같다.
void f()
{
std::shared_ptr<Investment> pInv = createInvestment(); // 팩토리 함수 호출
... // pInv 사용
}
사실 거의 똑같아 보이지만 여기서 pInv는 복사가 훨씬 용이하다.
std::shared_ptr<Investment> pInv1(createInvestment()); // createInvestment에서 반환된 객체
std::shared_ptr<Investment> pInv2(pInv1); // pInv1과 pInv2는 모두 같은 객체를 가리킴
pInv1 = pInv2; // 마찬가지임
자, 마지막으로 한가지 알아 두어야 할것은 이 auto_ptr과, shared_ptr은 소멸자에서 객체를 해제할때 delete를 쓴다는 것이다. 그렇기 때문에 delete[]를 이용하여 해제해야 하는 포인터를 생성하면 예상하지 못한 일이 발생할 수 있으니 아래와 같이 작성하는 일은 없도록 하자.
std::shared_ptr<std::string> pStr(new std::string[10]); // delete[]로 해제해야함
std::auto_ptr<int> pInts(new int[1024]); // 이것도 마찬가지
또 참고로 한가지 말을 보태자면
std::shared_ptr<Investment> pInv(new Investment);
이런식으로 자원을 생성함과 동시에 바로 객체를 초기화 하는 방식을 '자원 획득 즉 초기화'라고 해서 RAII(Resource Acquisition Is Initialization)이라고 부른다.