Chapter 3. 자원 관리
프로그래밍 분야에서 자원(resource)이란, 사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것을 일컫습니다.
ex) 동적 할당한 메모리, 파일 서술자, 뮤텍스 잠금, 그래픽 유저 인터페이스, 폰트, 브러시
항목 13. 자원 관리에는 객체가 그만
- 예로 팩토리 함수를 통해 얻어낸 객체를 사용할 때 삭제를 책임져야 하는 쪽은 함수의 호출인데 이 과정에서 해제가 원할히 일어나지 못하는 경우가 있을 수 있습니다.
1 | void f() |
-
자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는
RAII
객체를 사용합니다.- 자원을 획득한 후에 자원 관리 객체에게 넘깁니다.
RAII
: Resource Acquisition is Initialization, 자원 획득은 즉 초기화입니다. - 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 합니다.
- 자원을 획득한 후에 자원 관리 객체에게 넘깁니다.
-
일반적으로 널리 쓰이는
RAII
클래스std::auto_ptr
가리키고 있는 대상에 대해 소멸자가 자동으로delete
를 불러주도록 설계되어있습니다.
같은 객체를 가르키auto_ptr
이 둘 이상 존재하면 안 되기에 복사하면 원복 객체는null
로 만듭니다.
1
2
3
4std::auto_ptr<Investment> pInv1(createInvestment);
std::auto_ptr<Investment> pInv2(pInv1) //pInv2가 현재 그 객체를 가르키고, pInv1은 null입니다.
pInv1 = pInv2; // pInv1이 현재 그 객체를 가르키고, pInv2은 null 입니다.std::tr1::shared_ptr
RCSP(reference-counting smart pointer) : 참조 카운팅 방식 스마트 포인터
자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터입니다.
복사 생성자, 대입 연산자에 정상적으로 동작합니다.
1
2
3
4
5
6
7
8
9void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1은 반환된 객체를 가르킵니다.
std::tr1::shared_ptr<Investment> pInv2(pInv1); // pInv1, pInv2 모두 그 객체를 가르킵니다.
pInv1 = pInv2; // 마찬가지입니다.
} //pInv1, pInv2 가 소멸되어 가르키는 개수가 0이 되어 자원을 자동으로 삭제합니다.
항목 14. 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자.
RAII
객체의 복사는 관리하는 자원의 복사 문제를 안고 간다.
ex)Mutex
객체를 조작하는 경우 복사를 허용하면 안됩니다.- 복사 동작 구현 방법
- 복사를 금지합니다. (복사 연산을
private
멤버로 만듭니다.) - 관리하고 있는 자원에 대해 참조 카운팅을 수행합니다. (
std::tr1::shared_ptr
을 사용합니다.) - 관리하고 있는 자원을 진짜로 복사합니다. (깊은 복사(deep copy))
- 관리하고 있는 자원의 소유권을 옮깁니다. (
auto_ptr
의 복사 동작)
- 복사를 금지합니다. (복사 연산을
항목 15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자.
- 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문입니다.
- 변환 방법
- 명시적 변환(explicit conversion)
멤버 함수를 사용해 객체에 들어 있는 실제 자원에 접근합니다.
std::tr1::shared_ptr
과std::auto_ptr
은 get이라는 맴버 함수를 제공합니다. ex)pInv.get()
- 암시적 변환(implicit conversion)
암시적 변환 함수를 제공하여 자원 접근을 매끄럽게 할 수 잇도록 합니다.
1
2
3
4
5
6
7
8
9class Font {
public:
...
operator FontHandle() const
{return f;}
...
private:
Fonthandle f; // 실제 폰트 자원
}; - 명시적 변환(explicit conversion)
안전성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면 암시적 변환이 괜찮습니다.
항목 16. new 및 delete를 사용할 때는 형태를 반드시 맞추자.
-
new
표현식에[]
를 썻으면, 대응되는delete
표현식에도[]
를 써야 합니다. 마찬가지로new
표현식에[]
를 안 썻으면, 대응되는delete
표현식에서도[]
를 쓰지 말아야 합니다. -
메모리 배치구조
- 단일 객체
Object - 객체 배열
n Object Object Object Object n은 배열의 크기
-
delete
시[]
이 존재하면 n을 읽어 갯수만큼 소멸자를 호출하고 메모리를 해제한다.
배열 타입을 typedef 타입으로 만드는 경우 오류 발생의 소지가 있다.
1 | typedef std::string AddressLines[4]; //배열 타입 선언 |
항목 17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자.
1 | int priority(); |
- 매개 변수를 넘겨주는 연산
new Widget
,priority()
,tr1::shared_ptr 생성자
3가지가 실행됩니다. tr1::shared_ptr 생성자
는new Widget
을 매개변수를 사용하기 때문에 순서가 뒤로 보장 되지만 다른 연산들과의 순서는 보장되지 못합니다.new Widget
➡️priority()
➡️tr1::shared_ptr 생성자
순서로 실행 되는 경우priority()
호출 부분에서 예외가 발생하면 자원이 누출됩니다.
1 | std::tr1::shared_ptr<Widget> pw(new Widget); //독립적인 문장으로 실행 |
- 한 문장으로 만들지 않으면 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있습니다.