[C/C++] 스마트 포인터

개발 노트 2009. 1. 16. 16:57 posted by 무병장수권력자


작성자 : 김문규
최초 작성일 : 2009. 1.16

1. 들어가며
C++에도 자동으로 메모리를 관리해 주는 기능이 있다면, 사용하시겠습니까?
물론 사람마다 다를 꺼에요. 자바 개발자의 경우에는 프로그래밍을 시작하면서 부터 가비지 콜렉션에 대해서 이해하고 믿는 반면에 C 개발자 들은 어찌도 자기 자신만 믿으려 하는 지... 남이 해주는 건 다 못 믿어서 직접 짜려고만 하지요.
호호...저만 그런가요?

C++에도 일부 메모리를 관리해 주는 기능이 있으며,
세계 각국의 개발자들이 만든 3rd party 라이브러리를 사용한다면 좀 더 강력한 메모리 자동 관리 기능을 사용할 수 있습니다.


결론에 다시 말하겠지만,
본인이 실수 없이 완벽한 코딩을 할 수 있고 시간과 노력이 아깝지 않다면 직접 구현하십시오.
이 기능은 때와 개인의 취향에 따라 선택되어야 할 것입니다.


2. 소유권 독점 방식 Smart Pointer, auto_ptr
C++ 표준입니다. 그런데, 사용이 곤란합니다.

일단 권장하는 사용법부터 보실까요?
#include <memory>
using namespace std;
class CTest
{
public:
    inline int getValue() { return m_ivalue; }
    inline void setValue(int _iValue) { m_ivalue = _iValue; }
protected:
    int m_ivalue;
};
int main(void)
{
    auto_ptr<CTest> pTest(new CTest());
    pTest->setValue(3);
    printf("return = %d\n", pTest->getValue());
    return 0;
}

객체를 생성하고서 이를 가리키는 포인터를 auto_ptr로 얻습니다. 이를 사용하고 나서 굳이 delete를 호출하지 않아도 적당한 때(?)가 되면 자동으로 해당 메모리 영역이 해제됩니다.

좋아 보입니다.
주의할 점은 클래스나 구조체 같은 것에 사용 가능하지만 배열에 사용할 수 없다는 것입니다.
그리고, auto_ptr은 소유권 독점 방식의 smart pointer이기 때문에 사용이 다소 직관적이지 않습니다. 아래의 예를 보세요.

#include <memory>
using namespace std;
class CTest
{
public:
    inline int getValue() { return m_ivalue; }
    inline void setValue(int _iValue) { m_ivalue = _iValue; }
protected:
    int m_ivalue;
};
void modifyTest(auto_ptr<CTest> _pTest)
{
    _pTest->setValue(5);
};
int main(void)
{
    auto_ptr<CTest> pTest(new CTest());
    pTest->setValue(3);
    modifyTest(pTest);
    printf("return = %d\n", pTest->getValue());
    return 0;
}

적색으로 표시한 부분이 추가 되었습니다. 일반적인 포인터의 개념으로 바라본다면 전혀 문제가 없어야 합니다. 그런데, 결과는 segmentation error 입니다. 이유는 modify_ctest 가 호출되는 시점에 pTest의 소유권이 main에서 modify_ctest로 옮겨 졌기 때문입니다. 그래서, 소유권을 박탈당한 main은 pTest를 사용할 수 없게 되는 겁니다. 이를 Trasfer of ownership 이라고 합니다.
솔직히 참 어의 없다고 생각이 됩니다. 이걸 쓸 바에는 그냥 일반 포인터를 다시 사용하겠습니다.

3. 소유권 공유 방식, boost::shared_ptr
관련된 라이브러리는 http://www.boost.org 에서 사용법 확인 및 다운로드가 가능합니다.
설치 하셨다고 가정하고 설명하겠습니다.

당연히 boost::shared_ptr은 2절의 소유권 독점 방식의 문제점을 해결했습니다. 우리가 알고 있는 공유 개념의 포인터이면서도 메모리는 자동으로 관리되는~ 사랑스런 러블리 포인터 입니다.
그래서 다음의 코드는 정상적으로 동작하게 됩니다.

#include <boost/smart_ptr.hpp>

using namespace std;

class CTest
{
public:
    inline int getValue() { return m_iValue; }
    inline void setValue(int _iValue) { m_iValue = _iValue; }
protected:
    int m_iValue;
};

void modifyTest(boost::shared_ptr<CTest> _pTest)
{
    _pTest->setValue(5);
};

int main(void)
{
    boost::shared_ptr<CTest> pTest(new CTest());
    pTest->setValue(3);

    modifyTest(pTest);
    printf("return = %d\n", pTest->getValue());

    return 0;
}

원리는 이렇습니다.
boost::shared_ptr은 reference counter를 두어서 해당 shared_ptr을 사용하는 객체의 개수를 관리합니다. 이 reference counter의 값이 1이상이라면 이 메모리를 해제하면 안된다는 뜻이겠지요. counter가 0보다 클 때는 해당 메모리 영역을 유지하다가 counter가 0이 되면 이를 해제하는 것이지요.

원리야 어찌됬던 간에 auto_ptr의 사용시에 약간 어색했던 부분은 이제 해결되었습니다. 하지만 boost:shared_ptr 역시 배열에는 사용할 수 없다는 점에서는 동일합니다. 배열에 적용하기 위해서는 boost::shared_array를 사용하셔야 합니다.

** 주의 사항이 있습니다.
- unnamed shared_ptr을 사용하지 말 것
- 순환 참조하지 말 것
- 하나의 객체에 대한 포인터를 두개 이상의 shared_ptr로 가리키지 말 것
자세한 내용은 다른 포스트에서 다루도록 하겠어요

4. 배열에 적용 가능한 smart pointer , boost::shared_array

shared_ptr과는 다르게 new [] 로 만들어지는 메모리 영역을 관리할 수 있는 smart pointer 입니다.
개인적으로는 가장 많이 사용하고 싶은 녀석입니다. 일단 예제를 보지요.

#include <boost/smart_ptr.hpp>
using namespace std;
class CTest
{
public:
    boost::shared_array<char> allocMemory();
};
boost::shared_array<char> CTest::allocMemory() {
    int iSizeResult = 0;
    iSizeResult = 50;
    boost::shared_array<char> pMemoryCreatedInside(new char[iSizeResult]);  // 메모리 할당
    sprintf(pMemoryCreatedInside.get(), "%s", "I am MK!");

    // 반환해야 하기 때문에 할당한 메모리를 해제 해서는 안됩니다.
    return pMemoryCreatedInside;
}
int main(void)
{
    boost::shared_ptr<CTest> pTest(new CTest);
    boost::shared_array<char> pMemoryUsedOutside = pTest->allocMemory();

    // allocMemory에서 할당한 값을 사용합니다.
    printf("%s\n", pMemoryUsedOutside.get());

    // 일반 포인터였다면 다소 어색하지만 여기서 pMemoryUsedOutside를 해제해야 합니다.
    // 까 먹기 쉽상이지요.

    return 0;
}

이처럼, 라이브러리 기반으로 개발하다 보면 메모리의 할당자와 메모리의 참조자, 그리고 메모리의 해제자가 달라지는 경우에 대한 경우가 많은데 이 경우 메모리 해제 문제를 어떻게 처리할 지 고민하게 됩니다. shared_array와 shared_ptr은 이 경우에 훌륭한 해법을 제공합니다.

각자 나름의 방법이 있겠지만, 저는 이중 포인터(**)를 활용해서 최대한 메모리 할당자와 메모리 해제자 만큼은 같아지게 하려고 합니다. 전 할 때마다 어려워요~ 머리도 아파지고 ^^

그런데, shared_array를 사용하면 이런 복잡한 포인터를 쓸 필요가 없어진다는 거죠~
그냥 논리적으로 위치해야 하는 곳에서 생성하고~ 그대로 두면 알아서 해제되기 때문이지요~

5. 마치면서
많은 C/C++ 개발자는 자바 개발자를 때때로 부러워합니다. 그 큰 이유는 gabage colletor가 아닐까 합니다. 인간은 모두 실수를 저지르기 때문이겠지요. 하지만, smart pointer는 이런 요구 사항을 어느 정도는 만족시켜 주고 있습니다.
하지만, smart pointer도 아무때나 마구 사용할 수는 없습니다. 잘못 사용할 경우에는 오히려 심각한 메모리 문제를 일으킬 수도 있습니다.
이제, 선택은 저와 여러분에게 달려 있네요~. 사용해 보세요. 편한지 아님...문제 덩어리인지~