KoreanFoodie's Study
Effective C++ | 항목 13 : 자원 관리에는 객체가 그만! 본문
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]);
더 자세한 내용은 이 링크를 참조하자!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 15 : 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2022.10.25 |
---|---|
Effective C++ | 항목 14 : 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자 (0) | 2022.10.25 |
Effective C++ | 항목 12 : 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2022.10.25 |
Effective C++ | 항목 11 : operator= 에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2022.10.25 |
Effective C++ | 항목 10 : 대입 연산자는 *this 의 참조자를 반환하게 하자 (0) | 2022.10.25 |