프로그래밍에서 자원(resource)이란, 사용을 마치고 난 후엔 시스템에 돌려줘야 하는 모든 것을 일컫는다.
돌려주지 않는 순간 메모리 누수가 발생한다. 동적 할당이 대표적인 예인데 포폴작업하다보면 동적할당을 해제하지 않으면 점점 컴이 느려진다고 느껴지는데 메모리 할당 문제랑 연관이 있는진 몰라도 메모리 해제의 필요성을 느낀다.
자원에는 파일 서술자(file descriptor), 뮤텍스 잠금(mutex lock), 그래픽 유저 인터페이스(GUI)에서 쓰는 폰트(font)나 브러시(brush)도 자원이다. 어쨌든 메모리를 사용한 후엔 반드시 해제해야 한다.
투자를 모델링해 주는 클래스 라이브러리르 가지고 어떤 작업을 한다고 가정해보자.
이 라이브러리는 Investment라는 최상위 클래스가 있고, 이 클래스를 기본으로 하여 구체적인 형태의 투자 클래스로 파생되어 있다. 또한 Investment의 파생 클래스의 객체를 사용자가 얻어내는 용도로 팩토리 함수(7장 참고)만을 쓰도록 한다.
class Investment { ... } //투자를 모델링하는 클래스 계통의 최상위 클래스
//Investment클래스 계통에 속한 클래스의 객체를 동적 할당하고 그 포인터를 반환
//이 객체의 해제는 호출자 쪽에서 직접 해야한다.
Investment* createInvestment(param);
createInvestment함수를 통해 얻어낸 객첼르 사용할 일이 이제 없을 때 그 객첼르 삭제해야 하는 쪽은 이 함수의 호출자(caller)이다.
void function()
{
Investment* pInv = createInvestment(); //팩토리 함수 호출
...;
delete pInv; //객체 해제
}
아무 문제 없어 보이지만, createInvestment함수로 부터 얻은 투자 객체의 삭제에 실패할 수 있는 경우가 존재한다.
1.'...' 즉 객체를 사용하다가 어딘가에 return문을 만난경우(delete pInv;까지 도달하지 못한 경우)
2.createInvestment함수와 delete가 루프 안에 있고 continue 혹은 goto문에 의해 갑작스레 루프로부터 빠져나온 경우
3.객체를 사용하다 어떤 문장에서 예외가 발생하는 경우
경우의 수를 나열했지만 결론적으로 delete pInv;까지 도달하지 못했을 경우다.
createInvestment함수로 얻어낸 자원이 항상 해제되도록 만들 방법은, 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하고, 그 소멸자는 실행 제어가 function를 떠날 때 호출되도록 만드는 것이다(function함수 종료시).
소프트웨어 개발에 쓰이는 상당수의 자원이 힙에서 동적으로 할당되고, 하나의 블록 혹은 함수 안에서만 쓰이는 경우가 잦기 때문에 그 블록 혹은 함수로부터 실행 제어가 빠져나올 때 자원이 해제되는 게 맞다.
표준 라이브러리를 보면 auto_ptr이란 것이 있는데, 이런 용도에 쓰라고 마련된 클래스다. auto_ptr은 포인터와 비슷하게 동작하는 스마트 포인터(smart pointer)(*주 1)로써, 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 호출하도록 설계되어 있다.
void function()
{
auto_ptr<Investment> pInv(createInvestment());
...;
} //auto_ptr의 소멸자를 통해 pInv를 delete함
자원 관리에 객체를 사용하는 방법
1.자원을 획득한 후에 자원 관리 객체에게 넘긴다.
createInvestment함수가 만들어 준 자원은 그 자원을 관리할 auto_ptr 객첼르 초기화 하는데 쓰이고 있다. 이를 업계 용어로 자원 획득 즉 초기화(Resource Acquisition Is Initialization : RAII)라는 이름이다. 자원 획득과 자원 관리 객체의 초기화가 바로 한 문장에서 이루어지기 때문이다.
2.자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
소멸자는 어떤 객체가 소멸될 때 자동으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가 이루어지게 된다. 물론 객체가 해제하다가 예외가 발생될 수 있는 상황에 빠질순 있다(8장 참고).
auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 먹이기 때문에, 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 문제가 발생한다. 이런 사태가 발생하는 경우 자원 삭제가 두 번 하기에 프로그램은 에러에 빠질것 이다.
이런 에러를 처리하기 위헤서 auto_ptr은 특별한 특성을 갖고 있는데, auto_ptr은 복사 생성자 혹은 복사 대입연산자를 통해 복사를 하면 원본 객체는 NULL로 만든다. 한마디로 하나의 객체만이 그 자원의 유일한 소유권을 보장한다.
//pInv1이 가르키는 객체는 createInvestment함수에서 반환된 객체
auto_ptr<Investment> pInv1(createInvestment());
//pInv2는 현재 createInvestment함수에서 반환된 객체를 가리키고 있으며, pInv1은 NULL이다.
auto_ptr<Investment> pInv2(pInv1);
//pInv1은 createInvestment함수에서 반환된 객체를 가리키고 있으며, pInv2는 NULL이다.
pInv1 = pInv2;
auto_ptr은 복사를 하면 원본이 NULL이 된다는 장점이자 단점이 존재하기 때문에 최선이라는 느낌은 들지 않는다.
만약 auto_ptr을 사용할 수 없는 상황이라면 그 대안으로 참조 카운팅 방식 스마트 포인터(reference_counting stmart pointer : RCSP)가 괜찮다. RSCP는 특정한 자원을 참조하는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터이다. 카비지 컬렉션(gargabe collection)(*주 2)과 상당히 흡사하다.
TR1에서 제공하는 tr1::shared_ptr(54장 참고)이 대표적인 RCSP이다.
void function()
{
std::tr1::shared_ptr<Investment> pInv(createInvestment());
...
}
객체를 복사를 해도 이젠 NULL을 가리키지 않는다.
//pInv1이 카리키는 대상은 createInvestment함수에서 반환된 객체
std::tr1::shared_ptr<Investment> pInv1(createInvestment());
//pInv1 과 pInv2는 createInvestment함수에서 반환된 객체를 동시에 가리키고 있다.
std::tr1::shared_ptr<Investment> pInv2(pInv1);
//여전히 같은 객체를 가리키고 있다.
pInv1 = pInv2;
auto_ptr과 tr1::shared_ptr은 자원을 관리하는 방법들 중 몇가지일 뿐이고, 소멸자 내부에서 delete 연산자를 사용한다. delete[] 연산자가 아니다(16장 참고). delete[] 연산자가 아니기 때문에 동적 배열에 사용했을 땐 난감하다.
애석하게도 컴파일 에러도 발생하지 않는다.
C++ 표준 라이브러리에서는 동적 할당된 배열을 위해 준비된 스마트포인터가 제공되지 않는다.
왜냐하면 vector나 string으로 거의 해결이 가능하기 때문이다. 만약 배열에 사용할 수 있는 스마트 포인터가 필요하다면 부스트(55장 참고)에 boost::scoped_arry와 boost::shared_array가 구현되어있다.
이것만은 기억하자
자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 해제하는 RAII객체를 사용하자
일반적으로 널리 쓰이는 RAII 클래스는 tr1::shared_ptr 그리고 auto_ptr이다.
이 둘중 tr1::shared_ptr이 복사 시 동작이 직관적이기 떄문에 더 편하다. auto_ptr은 복사시 원본 객체를 NULL로 만든다.
(*주 1) 스마트 포인터
보통 new키워드를 사용해 기본 포인터(raw pointer)가 실제 메모리를 가리키도록 초기화한 후에, 기본 포인터를 스마트 포인터에 대입하여 사용한다.
정의된 스마트 포인터의 수명이 다하면, 소멸자는 delete키워드를 사용하여 자동으로 메모리를 해제한다.
따라서 new키워드가 반환하는 주소값을 스마트 포인터에 대입하면, 따로 메모리 해제할 필요가 없어진다.
C++11 이전에는 auto_ptr이라는 스마트 포인터를 사용하였으나 C++11부터는 3가지 스마트 포인터를 제공하고 있다.
1.unique_ptr : 하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터
해당 객체의 소유권을 갖고 있을 때만 소멸자가 해당 객체를 삭제할 수 있다.
move()멤버 함수를 통해 소유권을 이전할 수 있지만, 복사는 불가.
소유권이 이전되면 이전 unique_ptr인스턴스는 해당 객체를 소유하지 않게 된다.
C++14이후부터 make_unique()함수를 사용하면 unique_ptr인스턴스를 안전하게 생성이 가능하다.
예외 발생에 대해 안전하게 대처 가능
2.shared_ptr : 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지를 참조하는 스마트 포인터
참조하고 있는 스마트 포인터의 개수를 참조 횟수(reference count)라고 한다.
참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하며, 수명이 다 할떄마다 1씩 감소한다.
참조 횟수가 0이 되면 delete키워드를 사용하여 메모리를 자동으로 해제한다.
use_count()함수로 참조 횟수를 반환한다.
make_shared()함수를 사용하면 shared_ptr인스턴스를 안전하게 생성이 가능하다.
3.weak_ptr : 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만, 소유자의 수에는 포함되지 않는 스마트 포인터이다.
만약 서로가 상대방을 가리키는 shared_ptr을 갖고 있다면, 참조 횟수는 절대 0이 되지 않으므로 메모리는 영원히 해제되지 않는 상황이 발생하는데 이를 순환 참조(circular reference)라고 한다.
weak_ptr은 이러한 shared_ptr인스턴스 사이의 순환 참종를 제거하기 위해서 사용한다.
'서적 정리 > Effective C++' 카테고리의 다른 글
17.new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자 (0) | 2021.12.11 |
---|---|
16.new 및 delete를 사용할 때는 형태를 반드시 맞추자 (0) | 2021.12.11 |
15.자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2021.12.11 |
14.자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자 (0) | 2021.12.11 |
12.객체의 모든 부분을 빠짐없이 복사하자 (0) | 2021.12.10 |
11.operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2021.12.10 |
10.대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2021.12.10 |
9.객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (0) | 2021.12.10 |
댓글