자원 - 사용을 마치고 난 후에 시스템에 돌려주어야 하는 모든 것
돌려주지 않는 순간부터 문제가 생긴다.
가장 기본적인 자원은 동적 할당한 메모리를 들 수 있는데, 메모리는 관리해야 하는 많고 많은 자원 중 한 가지
자원에는 파일 서술자도 있고 뮤텍스 잠금도 있으며, 그래픽 유저 인터페이스에서 쓰이는 폰트와 브러시도 자원
수작업으로 해제하는 것이란 어떤 상황에서도 그리 녹녹치 않은 일
예외 발생도 고려해야 하고, return 문이 여러 개 들어 있는 함수도 만들어야 합니다.
자원 관리에는 객체가 멈춰야 한다.
함수를 통해 얻어낸 객체를 사용할 일이 없을 때 그 객체를 삭제해야 하는 쪽은 함수의 호출자
투자 객체의 삭제에 실패할 수 있는 경우가 있다. 첫 번째는 함수 어딘가에서 return 문이 들어 있을 가능성
return문이 실행되면 프로그램의 제어가 delete 문까지 도달하지 않게 된다.
혹은 continue나 goto문 처럼 실행해야 하는 곳을 빠져나왔을 때
어떤 문장에서 예외를 던질 수 있다는 점
예외가 던저지면 delete 문이 실행되지 않을 수도 있다.
이렇게 되면 담고 있는 메모리가 누출, 그와 동시에 그 객체가 갖고 있던 자원까지 모두 누출된다.
물른, 하나하나 따져 가면서 꼼꼼하게 프로그램을 만들면 이런 종류의 에러는 막을 수 있지만,
오랜 시간 동안 코드를 변경한다면 어떻게 할지 생각해 봐야 한다.
자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 함수를 떠날 때 호출되도록 만드는 것, 자원을 객체에 넣음으로써, C++가 자동으로 호출해 주는 소멸자에 의해 해당 자원을 저절로 해제할 수 있다.
상당수의 자원이 힙에서 동적으로 할당되고, 하나의 블록 혹은 함수 안에서만 씅이는 경우가 잦기 때문에 그 블록 혹은 함수로부터 실행 제어가 빠져나올 때 자원이 해제되는 게 맞다. 표준 라이브러리를 보면 auto_ptr이란 것이 있는데, 이런 용도에 쓰라고 마련된 클래스, auto_ptr은 포인터와 비슷하게 동작하는 객체[스마트 포인터]로서, 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계
자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징
- 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
자원 관리에 객체를 사용하는 아이디어에 대한 업계 용어도 자주 통용되고 있는데, 자원 획즉 즉 초기화라는 이름,
이런 이름이 나온 이유는 자원 획득과 자원 관리 객체의 초기화가 바로 한 문장에서 이루어지는 것이 너무나도 일상적이기 때문, 획득된 자원으로 자원 관리 객체를 초기화하지 않고 그 자원을 그 객체에 대입하는 경우도 종종 있기는 하지만, 어찌 됐든 "자원을 획득하고 나서 바로 자원 관리 객체에 넘겨 준다"는 점은 같다.
- 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 하자.
소멸자는 어떤 객체가 소멸될 때(유효범위를 벗어나는 경우가 한 가지 예) 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가 제대로 이루어지게 되는 것
객체를 해제하다가 예외가 발생될 수 있는 상황에 빠지면 사태가 많이 꼬이기도 한다.
(auto_ptr은 C++ 11이후에 삭제)
auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 먹이기 때문에, 어떤 객체를 가리키는 auto-ptr의 개수가 둘 이상이면 절대로 안 된다, 만에 하나 이러한 사태가 되면 자원이 두 번 삭제되는 결과를 낳게 되고, 프로그램은 미정의 동작이 발동 될 것이다. 그래서 auto_ptr은 상당히 유별난 특성을 지니고 있는데, auto_ptr 객체를 복사하면(복사 생성자 혹은 복사 대입 연산자를 통해) 원본 객체는 null로 만든다. 복사하는 객체만이 그 자원의 유일한 소유권을 갖는다고 가정
auto_ptr이 관리하는 객체는 두 개 이상의 auto_ptr 객체가 물고 있으면 안 된다는 요구사항까지 깔려 있는 통에, 동적으로 할당되는 모든 자원에 대한 관리 객체로서 auto_ptr을 쓰는 것은 최선이 아닐 것이다.
예를 들어, STL 컨테이너의 경우엔 원소들이 '정상적인' 복사 동작을 가져야 하기 때문에, auto_ptr은 이들의 원소로 허용되지 않는다.
auto_ptr을 쓸 수 없는 상황이라면 그 대안으로 참조 카운팅 방식 스마트 포인터가 괜찮다.
PCSP는 특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터
이것만 보면, RCSP의 동작은 가비지 컬렉션과 흡사하다.
참조 상태가 고리를 이루는 경우 (다른 두 객체가 서로를 가리키고 있다든지)를 없앨 수 없다는 점은 가비지 컬렉션과 다르다. TR1에서 제공되는 tr1::shared_ptr이 대표적인 RCSP, 이것을 쓰면 shared_ptr의 복사가 훨씬 자연스러워진다.
복사 동작이 예상대로 이루어지기 때문에, tr1::shared_ptr은 괴상한 복사 동작으로 인해 auto_ptr을 쓸 수 없는 STL 컨테이너 등의 환경에 딱 맞게 쓸 수 있다.
auto_ptr 및 tr1::shared_ptr은 소멸자 내부에서 delete 연산자를 사용, delete[] 연산자가 아니다.
말하자면, 동적으로 할당한 배열에 대해 auto_ptr이나 tr1::shared_ptr을 사용하면 문제가 된다. 심지어 컴파일 에러도 발생하지 않는다.
C++ 표준 라이브러리에서는 동적 할당된 배열을 위해 준비된 auto_ptr 혹은 tr1::shared_ptr 같은 클래스가 제공되지 않는다. 심지어 TR1에서도, 왜냐하면 동적으로 할당된 배열은 이제 vector 혹은 string으로 거의 대체할 수 있기 때문,
배열에 쓸 수 없는 auto_ptr이라든지 tr1::shared_ptr을 원한다면 부스트에 있다.
(boost::scoped_array, boost::shared_array)
자원 해제를 일일이 하다 보면 언젠가 잘못을 저지르고 만다. 이미 널리 쓰이고 있는 자원 관리 클래스를 활용하는 것도 조언을 쉽게 지킬 수 있다. 자원 관리 클래스를 직접 만드는 것도 좋다.
함수의 반환 타입이 포인터일 경우 이 부분 떄문에 문제가 생길 수 있다.
반환된 포인터에 대한 delete 호출을 호출자 쪽에서 해야 하는데, 그것을 잊어버리고 넘어가기 쉽기 떄문
* 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하자
'C++ > Effective' 카테고리의 다른 글
| 생성자, 소멸자, 대입 연산자(3) (0) | 2022.02.16 |
|---|---|
| 생성자, 소멸자, 대입 연산자(2) (0) | 2022.02.10 |
| 생성자, 소멸자, 대입 연산자(1) (0) | 2022.02.04 |
| 객체 초기화 (0) | 2022.01.31 |
| const (0) | 2022.01.25 |