[Effective C++] 항목 9 ~ 13
항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자
- 기본 클래스의 생성자가 호출될 동안에는 가상 함수는 절대로 파생 클래스로 내려가지 않음
- 기본 클래스 생성 과정에서는 가상 함수가 먹히지 않음
- 파생 클래스의 기본 클래스 부분이 생성되는 동안 그 객체의 타입은 기본 클래스
- 호출되는 가상 함수는 모두 기본 클래스의 것으로 결정
- 런타임 타입 정보를 사용하는 언어 요소(dynamic_Cast나 typeid)도 기본 클래스 타입의 객체로 취급
- 파생 클래스만의 데이터는 아직 초기화되지 않았으므로 없었던 것처럼 취급하는 것이 안전
- 생성자 호출 순서 : 기본 클래스 -> 파생 클래스
- 생성자나 소멸자에서 가상 함수를 호출하는지 잡아내는 일이 항상 쉽진 않다.
- 비가상 함수 내에 가상 함수가 호출되고 있는데 그걸 호출할 때
- 컴파일도 잘 되고 링크도 잘 됨. 순수 가상 함수가 아닐 때는 기본 클래스 버전 함수가 호출되어 오류 없이 실행(논리적 오류 발생)
- 대처 방법 : 가상 멤버 함수를 비가상 멤버 함수로 바꾸고 파생된 클래스의 생성자들로 하여금 필요한 로그 정보를 생성자로 넘겨야 한다는 규칙을 만든다.
항목 10 : 대입 연산자는 *this의 참조자를 반환하게 하자
- 대입 연산
- 대입 연산은 사슬처럼 엮일 수 있다. ex) x = y = z = 15;
- 대입 연산은 우측 연관(right-associative) 연산 ex) x = (y = (z = 15));
- 대입 연산자는 좌변 인자에 대한 참조자를 반환하도록 구현되어 있다.(관례)
- 모든 형태의 대입 연산자에서 지켜져야 한다.
항목 11 : operator = 에서는 자기 대입에 대한 처리가 빠지지 않도록 하자
- 자기 대입 : 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것.
- 중복 참조(여러 곳에서 하나의 객체를 참조)로 인해 자기 대입이 생길 수 있다.
- 같은 타입 객체 여러개를 참조자 / 포인터로 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려해야 한다.
- 처리법
- 일치성 검사
- ex)
if(this == &rhs) return *this;
- 예외에 안전하지 못할 수 있다.
- ex)
- 문장 순서 조정
- ex)
delete pb; pb = new Bitmap(*rhs.pb); return *this;
- this와 rhs가 같을 경우 문제가 생긴다.
Bitmap *pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; // 삭제 해도 무관할 때 삭제 return *this;
- ex)
- 복사 후 맞바꾸기(copy and swap)
- ex)
Widget temp(rhs); swap(temp); return *this;
- ex2)
Widget& Widget::operator=(widget rhs) { swap(rhs); return *this; }
- ex)
- 일치성 검사
항목 12 : 객체의 모든 부분을 빠짐 없이 복사하자
- 복사 함수(copying function) : 복사 생성자, 복사 대입 연산자
- 컴파일러가 생성한 복사 함수는 복사되는 데이터가 갖는 데이터를 빠짐없이 복사함.
- 클래스 수정으로 인해 커스텀 복사 함수가 완전 복사 -> 부분 복사가 되어도 컴파일러는 경고 하나 띄우지 않음
- 클래스에 데이터 멤버 추가 시 복사함수 / 생성자 / 비표준형 operator= 갱신 필요
- 상속시 파생 클래스의 데이터 뿐 아니라 기본 클래스의 데이터도 복사해줄 것
- 복사 함수 안에서 기본 클래스의 대응되는 복사 함수를 호출할 것
- 복사 함수의 코드 중복을 피하기 위해 한쪽에서 다른 한쪽을 호출하는 건 어불성설
- 복사 대입 연산자에서 복사 생성자 호출 : 이미 존재하는 객체를 생성한다?
- 복사 생성자에서 복사 대입 연산자 호출 : 초기화된 객체에 값을 주는건데 생성중인 객체에?
- 해결법 : 양 쪽에서 겹치는 부분을 별도의 멤버 함수로 분리하여 호출
항목 13 : 자원 관리에는 객체가 그만!
- 생성 후 삭제가 실패할 수 있는 경우가 무궁무진하다.
- 중간에 return문이 들어있는 경우
- delete가 루프 안에 있는데 continue 혹은 goto로 갑작스래 빠져나왔을 경우
- 중간에 예외가 발생할 경우
- 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하여 소멸자가 유효 범위를 벗어날 시 호출하도록 한다.
- auto_ptr
- 자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징
- 자원을 획득한 후에 자원관리 객체에게 넘긴다.(자원 획득 즉 초기화)(RAII)
- 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
- auto_ptr은 자신이 소멸될 때 가리키는 대상을 자동으로 delete 시키므로 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 안된다.
- auto_ptr 객체를 복사하면 원본 객체를 null로 만든다.
- 대안 : 참조 카운팅 방식 스마트 포인터(RCSP)
- 레퍼런스 카운터 계산으로 자원 관리. 참조 상태가 고리를 이루면 없앨 수 없다.(가비지 컬렉션과 차이)
- shared_ptr
- auto_ptr 및 shared_ptr은 내부 소멸자에서 delete[]가 아닌 delete 연산자를 사용한다.
- 동적 할당 배열에는 사용할 수 없다.
- 그러나 사용해도 컴파일 에러가 발생하지 않는다.
-
auto_ptr, shared_ptr로도 제대로 관리할 수 없는 자원의 경우 자원 관리 클래스를 직접 만들 수 밖에 없다.
- 팩토리 메소드의 반환 타입이 포인터면 호출자 쪽에서 delete 호출을 해줘야 하는데 이 부분을 개선 할 수 있다.(18)