[Effective C++] Chapter 3. 자원 관리
Table of Contents
이펙티브 C++ 책을 읽고 공부한 노트입니다.
Item 13: 자원 관리에는 객체가 그만! #
- 프로그래밍 분야에서, 자원(resource) 이란?
- 사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것이다.
- 동적 할당 메모리, 파일 서술자(file descriptor), 뮤텍스 잠금(mutex lock), GUI에서 쓰이는 폰트나 브러시, 데이터베이스 연결, 네트워크 소켓 등
- 어떻게 하면 팩토리 함수로 얻은 자원이 항상 해제되도록 할 수 있을까?
class Investment {};
Investment * createInvestment(); // 팩토리 함수
void f()
{
Investment * pInv = createInvestment(); // Investment를 생성한다.
// ...
// 도중하차되어 여기까지 코드가 도달하지 않으면 어떡할 것인가?
delete pInv;
}
자원 관리 객체 #
- 객체를 써서 자원을 관리하는 게 최고다.
- 자원을 객체에 넣음으로써 C++이 자동으로 호출해 주는 소멸자 덕분에 자원이 저절로 해제되도록 할 수 있다.
- 자원 관리 객체의 사용 방법
- (1) 자원을 획득한 후에 자원 관리 객체에게 넘긴다. (Resource Acquisition Is Initialization; RAII)
- (2) 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
스마트 포인터 #
- 이런 객체 중에 하나가 바로 스마트 포인터(smart pointer) 이다.
auto_ptr
은 블록을 벗어나면 소멸자가 자동으로delete
를 불러주도록 설계되어 있다.
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
// ...
} // auto_ptr이 소멸자를 통해 delete pInv를 한다.
auto_ptr
은 객체를 복사하면 원본 객체는null
로 바뀐다.
std::auto_ptr<Investment> pInv1(createInvestment());
// pInv2가 Investment를 가리키고, pInv1은 null이다.
std::auto_ptr<Investment> pInv2(pInv1);
// pInv1이 Investment를 가리키고, pInv2는 null이다.
pInv1 = pInv2;
- 참조 카운팅 방식 스마트 포인터(reference-counting smart pointer: RCDP) 인
shared_ptr
를 사용하면 여러 포인터가 객체를 가리키도록 할 수 있다.shared_ptr
은 가리키는 외부 객체의 수가 0이 되면 해당 자원을 자동으로 삭제한다.
std::shared_ptr<Investment> pInv1(createInvestment());
// pInv1와 pInv2 모두 Investment를 가리킨다.
std::shared_ptr<Investment> pInv2(pInv1);
pInv1 = pInv2;
- 스마트 포인터에서 주의할 점
delete
를 부르는 것이지delete []
를 부르는 것이 아니다.- 따라서 아래와 같은 코드는 문제가 발생한다.
- 배열을 위한 부스트(
boost::scoped_array
,boost::shared_array
)는 Item 55를 참고하자.
std::auto_ptr<std::string> aps(new std::string[10]);
std::shared_ptr<int> apt(new int[1024]);
Item 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자 #
- 모든 자원은 힙에 생기지 않으므로,
delete
를 알아서 해주는 스마트 포인터로는 다른 자원 관리가 불가능하다.- 그렇다면 직접 RAII 법칙에 맞는 자원 관리 클래스를 만들어볼 수 있겠다.
- 예를 들어, 뮤텍스 잠금을 관리하는 클래스를 만든다고 하자.
class Lock
{
private:
Mutex * mutexPtr;
public:
explicit Lock(Mutex * pm) : mutexPtr(pm)
{
lock(mutexPtr); // 잠금
}
~Lock()
{
unlock(mutexPtr); // 해제
}
};
// 사용자는 이렇게 사용하면 되겠다.
int main()
{
Mutex m;
{
Lock lock(&m); // 잠금을 건다.
// ... 임계 영역에서 연산을 수행한다.
} // 블록의 끝이므로 ~Lock()소멸자가 자동으로 불려서 잠금이 해제된다.
}
자원 관리 객체의 복사 처리 #
- 하지만
Lock
객체가 복사된다면 어떻게 해야 할까? - (1) 복사를 금지한다.
class Lock : private Uncopyable {}; // Item 6 참고
- (2) 참조 카운팅을 수행한다.
shared_ptr
은 삭제자(delete) 지정을 허용한다. 그래서 소멸자에서delete
를 대신해 불릴 함수를 지정할 수 있다.auto_ptr
은 이런 기능이 없다.
class Lock
{
private:
std::shared_ptr<Mutex> mutexPtr;
public:
explicit Lock(Mutex * pm)
: mutexPtr(pm, unlock) // 삭제자를 unlock으로 지정한다.
{
lock(mutexPtr.get()); // 잠금
}
};
- (3) 자원을 진짜로 복사한다.
- 깊은 복사(deep copy)를 수행해야 하겠다.
- (4) 자원의 소유권을 옮긴다.
auto_ptr
의 복사 동작과 같다.
Item 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 #
- 외부에서 자원 관리 클래스의 자원에 접근할 수 있도록 해야할 경우가 있다.
// Item 13에서 가져온 예제
// 투자금이 유입된 이후로 경과한 날수를 계산한다.
int daysHeld(const Investment * pi);
void f()
{
std::shared_ptr<Investment> pInv(createInvestment());
int days = daysHeld(pInv);
// 이렇게 사용하고 싶지만
// pInv는 std::shared_ptr<Investment> 이고
// 매개변수 형식은 Investment * 이다.
// shared_ptr의 자원에 접근할 수 있어야 하겠다.
}
RAII 클래스를 자원으로 변환하는 방법 #
- 명시적 혹은 암시적 변환 함수를 제공한다.
- 어떤 함수를 사용할지는 사용 용도 및 환경에 따라 달라지겠다.
- 늘 그렇지는 않지만 명시적 변환 함수를 제공하는 쪽이 나을 때가 많다.
- RAII 클래스는 애초에 데이터 은닉이 목적이 아니므로, 자원을 공개한다고 해서 캡슐화에 위배되는 것은 아니다.
- (1) 명시적 변환(explicit conversion)
shared_ptr
과auto_ptr
은get()
이라는 멤버 함수를 제공한다. 이것으로 실제 자원을 가리키는 포인터의 사본을 얻을 수 있다.
int days = daysHeld(pInv.get()); // 동작한다.
- (2) 암시적 변환(implicit conversion)
shared_ptr
과auto_ptr
은 포인터 역참조 연산자(operator->
,operator*
)를 오버로딩하고 있다.
class Investment
{
public:
bool isTaxFree() const;
};
Investment * createInvestment();
int main()
{
std::shared_ptr<Investment> pi1(createInvestment());
bool isTaxFree1 = pi1->isTaxFree(); // operator->
std::auto_ptr<Investment> pi2(createInvestment());
bool isTaxFree2 = (*pi2).isTaxFree(); // operator*
}
- 두 변환의 예시
class FontHandle {};
class Font
{
private:
FontHandle f;
public:
FontHandle get const // 명시적 변환 함수
{
return f;
}
operator FontHandle() const // 암시적 변환 함수
{
return f;
}
};
FontHandle getFont();
void changeFontSize(FontHandle f, int newSize);
int main()
{
Font f(getFont());
// (1) 명시적 변환
changeFontSize(f.get(), 13);
// (2) 암시적 변환
changeFontSize(f, 13); // Font 형식인 f가 FontHandler로 변환된다.
// 원치 않는 경우가 발생할 수도 있다.
FontHandle fh = f;
// Font를 복사하고 싶었는데
// f가 FontHandle로 바뀐 후 복사된다.
}
Item 16: new
및 delete
를 사용할 때는 형태를 반드시 맞추자 #
new
연산자를 사용하면 벌어지는 일들- (1) 메모리가 할당된다. (
operator new
함수가 사용된다) - (2) 생성자가 호출된다.
- (1) 메모리가 할당된다. (
delete
연산자를 사용하면 벌어지는 일들- (1) 소멸자가 호출된다.
- (2) 메모리가 해제된다. (
operator delete
함수가 사용된다)
new
를 사용해서 힙에 만들어진 단일 객체의 메모리 배치구조는, 객체 배열에 대한 메모리 배치구조와 다르다.- 힙의 맨 앞에 배열원소의 개수가 박혀 들어간다.
- 이걸로
delete
는 소멸자를 몇 번 부를지를 알 수 있다.
- 따라서
new []
표현식을 썼으면,delete []
표현식을 맞춰 써야한다.
std::string * sPtr1 = new std::string;
std::string * sPtr2 = new std::string[100];
delete sPtr1;
delete [] sPtr2;
- 왠만하면 배열 타입을
typedef
타입으로 만들지 말자.delete []
를 써야 하는지 헷갈린다.vector
타입으로 만드는게 낫겠다.
typedef std::string AddressLines[4];
int main()
{
std::string * pal = new AddressLines; // new string[4];
delete pal; // 이거 아니다.
delete [] pal; // 이거 써야되는데 헷갈리지 않는가?
}
Item 17: new
로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자 #
class Widget {};
int getPriority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
int main()
{
// 컴파일 에러 :
// shared_ptr의 생성자는 explicit이라서 암시적 변환이 안 된다.
processWidget(new Widget, getPriority());
// 이것은 위험하다 :
// new Widget으로 만들어진 자원이 shared_ptr에 저장되기 전, 그 사이에
// getPriority()가 불리고 예외가 발생했다면
// 만들어진 자원은 유실되고 만다.
processWidget(std::shared_ptr<Widget>(new Widget), getPriority());
}
- 따라서,
new
로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 문장으로 만들어야 하겠다.
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, getPriority());