본문 바로가기

C++/Effective C++

[C++] 13. 자원 관리에는 객체가 그만!

자원 관리

C++에서 말하는 자원 관리는 대부분 메모리 누수를 방지하기 위한 방법에 대해서 이야기한다고 생각한다. 당연히 이번에도 그러한 내용을 다룬다.

class Investment { ... };
void fun()
{
    Investment *pInv = createInvestment(); // 팩토리 함수 호출
    ...
    delete pInv;
}

 

해당 코드를 보게 되면 문제없이 pInv를 메모리에서 해제하는 것처럼 보이지만, 만약 … 부분, 즉 중간 부분에 루프문이나 예외문과 같은 코드를 작성하여 도달하지 못하게 된다면 메모리 누수가 발생하게 된다.

스마트 포인터(smart pointer): auto_ptr

소프트웨어 개발에 쓰이는 상당수의 자원이 힙에서 동적으로 할당되고, 하나의 블록(block) 혹은 함수 안에서만 쓰이는 경우가 잦기 때문에 그 블록 혹은 함수로부터 실행 제어가 빠져나올 때 해제시키기 위해 auto_ptr을 사용한다.

class Investment { ... };
void fun()
{
    std::auto_ptr<Investment> pInv(createInvestment()); // 팩토리 함수 호출
    ...
}

 

이러한 방법은 간단하지만 중요한 두 가지 특성을 가질 수 있다. 첫째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다. 둘째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.

auto_ptr의 문제점

std::auto_ptr<Investment>
pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1);
pInv1 = pInv2; // pInv2는 null

 

auto_ptr은 유별난 특성을 지니는데, 어떤 객체를 가리키는 auto_ptr이 둘 이상 되면 절대 안 되기 때문에 객체를 복사하게 되면 원본 객체는 null로 만든다.

참조 카운팅 방식 스마트 포인터(reference-counting smart pointer: RCSP)

RCSP는 특정한 어떤 자원을 가리키는(참조하는) 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터 입니다. 이러한 특징은 가비지 컬렉션(garbage collection)의 그것과 상당히 흡사합니다. 단, 참조 상태가 고리를 이루는 경우(예를 들면 다른 두 객체가 서로를 가리키고 있다든지)를 없앨 수 없다는 점은 가비지 컬렉션과 다릅니다.
void fun()
{
    ...
    std::tr1::shared_ptr<Inverstment> pInv1(createInvestment());
    std::tr1::shared_ptr<Inverstment> pInv2(createInvestment());
    
    pInv1 = pInv2;
    ...
}

 

TR1에서 제공되는 tr1::shared_ptr이 대표적인 RCSP로, 이를 통해 위와 같이 작성할 수 있다. 이러한 코드는 참조를 카운팅하는 방식으로 복사 동작이 예상대로 이루어진다.

주의 사항

std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);

 

auto_ptr 및 tr1::shared_ptr은 소멸자 내부에서 delete 연산을 사용한다. 즉, 배열을 위해 delete[] 연산을 따로 하지 않는다는 것이다.

추가로 C++ 표준 라이브러리에서는 동적으로 할당된 배열을 위해 준비된 auto_ptr 혹은 tr1::shared_ptr 같은 클래스가 제공되지 않는다. 심지어 TR1에서도 마찬가지다. 

 

이는 동적으로 할당된 배열이 이제 vector 및 string으로 거의 대체될 수 있기 때문이다. 이러한 기능을 사용하고 싶다면 Boost에 boost::scoped_array와 boost::shared_array가 있다.

자, 이제 마지막 잔소리입니다. 앞에서 본 createInvestment 함수의 반환 타입이 포인터로 되어 있는데, 이 부분 때문에 문제가 생길 수 있음을 지적하고 싶습니다. 반환된 포인터에 대한 delete 호출을 호출자 쪽에서 해야 하는데, 그것을 잊어버리고 넘어가기 쉽기 때문입니다(auto_ptr 혹은 tr1::shared_ptr을 써서 delete를 수행한다고 하더라도, createInvestment의 반환 값을 스마트 포인터에 저장해야 한다는 점만은 여전히 기억하고 있어야 하거든요). 이문제를 어떻게든 해결하려면 createInvestment를 수술해서 인터페이스를 고쳐야 하는데, 항목 18에서 해 두었으니 한 번 보세요.