어떠한 자원을 얻어냈다면 반드시 반환하는 것이 원칙이다.
자원을 객체에 넣고 그 자원의 해제를 객체의 소멸자가 맡도록 하고, 그 소멸자가 자원을 얻어낸 부분을 탈출할 때 호출되도록 만드는 것이 좋다.
자원을 획득하여 자원 관리 객체에 넘긴다 (RAII)
자원 관리 객체는 소멸자를 활용해 자원이 확실히 해제되도록 한다
스마트 포인터인 std::auto_ptr가 이 아이디어를 구현하고 있다.
어떠한 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 삭제를 두번 하게 되므로 절대로 안된다.
그래서 auto_ptr은 객체를 복사하면 원본 객체를 null로 만든다.
이러한 특성 때문에 원소가 정상적으로 복사 동작을 해야하는 STL 컨테이너의 원소로는 auto_ptr을 사용할 수 없다.
만약 이러한 특성때문에 auto_ptr을 사용하기 꺼려질 때에는 참조 카운팅 방식 스마트 포인터인 RCSP를 사용할 수 있다.
std::shared_ptr이 대표적인 rcsp이다.
shared_ptr은 stl의 원소로도 사용할 수 있다.
이 두 포인터는 소멸자 내부에서 delete[]가 아니라 delete를 사용하므로, 동적으로 할당한 배열에 대해서 사용하는 것은 좋지 않다.
동적 배열은 이제 vector와 string으로 거의 대체되므로 괜찮을 것이다.
만약 정말 필요하다면 boost::scoped_array와 boost::shared_array를 참조하라
auto_ptr과 shared_ptr은 힙에 생성되는 객체를 처리할 수 있는 스마트 포인터이다.
만약 힙에 생성되지 않는 자원은 어떻게 해야할까?
예를 들면 뮤텍스의 lock과 같은 것 말이다. 잠금을 잊지 않고 풀어줘야 하기 때문이다.
그럴땐 다음과 같이 직접 RAII를 적용한 클래스를 만든다.
class Lock{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
}
만약 이 객체에 대해 복사를 시도할 때에는 동작을 어떻게 정의해야 할까?
복사를 금지한다. 복사 연산자를 private으로 만들면 된다.
shared_ptr과 같이 관리 자원에 대해 참조 카운팅을 수행한다. shared_ptr은 원래 참조 카운트가 0이 되면 해당 객체를 삭제하지만, deleter를 정의해 해당 동작을 지정할 수 있다.
class Lock{
public:
explicit Lock(Mutex *pm) : mutexPtr(pm, unlock)
{
lock(mutexPtr.get());
}
private:
std::shared_ptr<Mutex> mutexPtr;
}
진짜로 복사한다. deep copy를 통해 해당 자원까지 복사시킨다.
관리중인 자원의 소유권을 옮긴다. auto_ptr처럼 말이다.
RAII 객체에서 직접 실제 자원으로 변환할 방법이 필요하다.
명시적 변환과 암시적 변환 두가지 방법이 있다.
명시적 변환: get()과 같은 함수로 자원의 포인터를 얻을 수 있도록 한다
암시적 변환: ->와 *의 연산자 오버로딩으로 자원에 접근하도록 만들거나 암시적 변환 함수를 직접 제공하도록 한다
명시적 변환이 귀찮다 하더라도 암시적 변환은 원하지 않는 타입 변환이 일어날 수 있는 가능성을 키우므로, 시의 적절하게 사용하자.
이러한 접근 방법을 열어주는 것이 캡슐화에 위배될까 걱정은 하지말자.
애초에 자원 관리 객체의 목적은 은닉이 아니다!
shared_ptr처럼 참조 카운팅 메커니즘만 잘 은닉되면 좋은 설계로 보여진다.
new 연산자를 사용해서 어떤 객체를 동적 할당하면, 이로 인해 두 가지의 내부 동작이 진행된다.
delete 연산자를 사용하면 또 다른 두 가지의 내부 동작이 진행된다.
여기서 질문.
delete 연산자가 적용되는 객체는 몇 개나 될까?
음??
다시 질문하자면, 삭제되는 포인터는 객체 하나만 가리킬까, 아니면 객체의 배열을 가리킬까?
delete가 '포인터가 배열을 가리키고 있구나' 라는 걸 알게 해주려면 아래 코드처럼 []
대괄호 쌍을 delete 뒤에 붙여줘야 한다.
delete는 앞쪽의 메모리 몇 바이트를 읽고 이것을 배열 크기라고 해석하고, 배열 크기에 해당하는 횟수만큼 소멸자를 호출하기 시작한다.
string *stringPtr1 = new string;
string *strintPtr2 = new string[100];
delete stringPtr1;
delete [] stringPtr2;
Q. 아래 코드처럼, 처리 우선순위를 알려주는 함수가 하나 있고, 동적으로 할당한 Widget 객체에 대해 어떤 우선순위에 따라 처리를 적용하는 함수가 하나 있다고 가정하겠습니다.
이 때Widget
객체를 어떻게 생성하면 좋을지 1~3번 중에서 골라주세요 (객관식)
// 문제 코드
int priority();
processWidget(shared_ptr<Widget> pw, int priority);
// 1~3번 중에서 골라주세요
1번. processWidget(shared_ptr<Widget>(new Widget), priority());
2번. processWidget(shared_ptr<Widget>(new Widget), priority());
3번. shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
와우,,