KoreanFoodie's Study

Effective C++ | 항목 13 : 자원 관리에는 객체가 그만! 본문

Tutorials/C++ : Advanced

Effective C++ | 항목 13 : 자원 관리에는 객체가 그만!

GoldGiver 2022. 10. 25. 16:09

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

1. 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자
2. 일반적으로 널리 쓰이는 RAII 클래스는 shared_ptr 그리고 auto_ptr 이다. 이 둘 가운데 shared_ptr 이 복사 시의 동작이 직관적이기 때문에 대개 더 좋다. 반면, auto_ptr 은 복사되는 객체(원본 객체) 를 null 로 만들어 버린다. -> auto_ptr 은 C++ 11 이후로 쓸 수 없게 됨! 대신 unique_ptr 를 사용한다

 

스마트 포인터를 사용하지 않고, '전통적인' 방식으로 메모리를 할당하고 해제하는 예시를 보자.

class Investment { ... };

// 팩토리 함수
Investment* CreateInvestment()
{
    return new Investment();
}

void f()
{
    // 메모리 할당
    Investment *pInv = CreateInvestment();
    
    // pInv 사용
    ...
    
    // 객체 해제
    delete pInv;
}

하지만 위의 f 함수의  ... 문에서 예외가 발생하거나, return 문이 실수로라도 불리게 되면, pInv 의 메모리가 해제되지 않을 수 있다. 대신 스마트 포인터를 사용하여 자원 누출을 확실하게 막을 수 있다.

 void f()
 {
   std::unique_ptr<Investment> pInv(CreateInvestment());
 }

자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징을 정리하면,

첫째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다. 자원 획득은 즉 초기화이다(Resource Acquisition Is Initialization: RAII).

둘째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다. 소멸자는 어떤 객체가 소멸될 때 (유효범위를 벗어나는 경우가 한 가지 예) 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는지에 상과없이 자원 해제가 제대로 이루어질 수 있다.

다음 예시를 보자.

std::unique_ptr<Investment> pInv1(CreateInvestment());
std::unique_ptr<Investment> pInv2(std::move(pInv1));
if (pInv1 == nullptr) cout << "pInv1 is nullptr!" << endl;
pInv1 = std::move(pInv2);
if (pInv2 == nullptr) cout << "pInv2 is nullptr!" << endl;

unique_ptr 는, std::move 를 통해 소유권을 변경할 수 있으며, 소유권이 변경되면 기존에 해당 객체를 소유했던 포인터는 nullptr 가 된다. 또한, std::move 를 사용하지 않고 복사 생성자의 인자로 unique_ptr 를 넣거나,  '=' 을 사용해 복사 대입연산자를 사용하려고 하면 해당 함수가 'deleted' 되었다고 하며 오류가 난다.

unique_ptr 대신 shared_ptr 를 쓰는 상황도 있는데, shared_ptr 는 참조 카운팅 방식 스마트 포인터(reference-counting smart pointer: RCSP) 로, 해당 객체를 참조하는 카운터가 0 이 되면 메모리가 해제되는 방식이다. 

위의 예시는 이렇게 바뀐다.

std::shared_ptr<Investment> pInv1(CreateInvestment());
std::shared_ptr<Investment> pInv2(pInv1);
pInv1 = pInv2;
// Reference Count 는 둘 다 2가 된다.
std::cout << "Reference Count of pInv1 : " << pInv1.use_count() << std::endl;
std::cout << "Reference Count of pInv2 : " << pInv2.use_count() << std::endl;

오히려, shared_ptr 를 쓸 때 std::move 를 쓰면 pInv1 과 pInv2 가 nullptr 가 되기도 하는 신기한 상황이 생긴다!

마지막으로, unique_ptr 및 shared_ptr 는 소멸자 내부에서 delete 연산자를 사용한다. delete [ ] 연산자가 아니다! 따라서, 동적으로 할당한 배열에 대해 unique_ptr 와 shared_ptr 를 사용하지 말아야 한다. 

// 아래 코드는 메모리 Leak 이 발생한다
std::unique_ptr<std::string> aps(new std::string[10]);
// 아래 코드는 메모리 해제가 잘 되지 않을 것이다
std::shared_ptr<int> spi(new int[1024]);

하지만, C++ 17 이후부터는 배열을 인자로 해도 삭제가 가능한 문법이 생겼는데... 다음과 같이 쓰면 된다.

std::shared_ptr<int[]> sp(new int[10]);

더 자세한 내용은 이 링크를 참조하자!

Comments