생성자, 소멸자 및 대입 연산자
- 생성자: 초기화 함수
- 소멸자: 객체 소멸 및 메모리 해제 과정 제어
- 대입 연산자: 기존 객체에 다른 객체의 값을 할당
항목 5
컴파일러는 클래스에 대해 생성자 소멸자 등을 암시적으로 생성함
컴파일러가 알아서 선언하는 것 (기본형, public, inline)
- 복사 생성자
- 복사 대입 연산자
- 소멸자 (비가상)
- 기본 생성자 (생성자가 없을 경우)
만들어진 기본 생성자, 소멸자가 하는 일
- 컴파일러에게 배후의 코드를 깔 수 있는 자리를 마련(기본 클래스 및 비정적 데이터 멤버의 생성자와 소멸자를 호출하는 코드)
만들어진 복사 생성자, 복사 대입 연산자가 하는 일
- 원본 객체의 비정적 데이터를 사본 객체 쪽으로 그냥 복사하는 것이 전부
복사 대입 연산자
- 생성되는 최종 결과 코드가 legal + resonable 해야함 (그렇지 않으면 컴파일러는 자동 생성을 거부함)
- 참조자와 상수와 같은 변수는 대입할 수 없기 때문
- private로 선언한 기본 클래스로부터 파생된 클래스의 경우, 이 클래스는 암시적 복사 대입 연산자를 가질 수 없음 (파생 클래스가 기본 클래스 접근 권한이 없기 때문)
- 생성되는 최종 결과 코드가 legal + resonable 해야함 (그렇지 않으면 컴파일러는 자동 생성을 거부함)
항목 6
암시적으로 생성된 함수가 필요없으면 사용 불가능하게 해야함
delete or default 사용
private 선언(구식)
- 그 클래스의 멤버 함수 및 friend 함수가 호출할 수 있다는 문제 (함수를 정의(구현)하지 않고, private 선언만)
- 링크 시점 에러를 컴파일 시점 에러로 옮길 수 있음
- 기본클래스에 private 선언하여 파생시킴 (기본클래스는 복사 방지만 맡는다는 특별한 의미)
- public일 필요가 없음, 가상 소멸자일 필요가 없음, EBO 기법 가능하지만 다중 상속 가능성이 있음
항목 7
다형성을 가진 기본 클래스에는 반드시 가상 소멸자 선언 (가상 함수 하나라도 있으면 소멸자 또한 가상으로)
기본 클래스로 설계되지 않았가너 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야함
(팩토리 함수: 새로 생성된 파생 클래스 객체에 대한 기본 클래스 포인터를 반환하는 함수)
기본 클래스가 비가상 소멸자일 때, 삭제 하게되면 기본 클래스 부분만 소멸됨 (파생 클래스 부분은 유지)
하지만, 기본 클래스로 의도하지 않은 클래스에 대해 소멸자를 가상으로 선언하는 것은 좋지 못함.
가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조가 하나 필요
- 이 자료구조는 프로그램 실행 중에 주어진 객체에 대해 어떤 가상 함수를 호출해야 하는지를 결정하는 데 쓰이는 정보
- 이는 포인터 형태로 vptr(가상 함수 테이블 포인터, virtual table pointer)라고 불림
- vptr은 가상 함수의 주소, 포인터들의 배열을 가리킴
- 가상함수 테이블 포인터의 배열은 vtbl(virtual table) 라고 불림
- 가상 함수를 하나라도 가지고 있는 클래스는 반드시 그와 관련된 vtbl을 가짐
- 어떤 객체에 대해 어떤 가상 함수가 호출되려고 하면, 호출되는 실제 함수는 그 객체의 vptr이 가리키는 vtbl에 따라 결정됨(vtbl에 있는 함수 포인터들 중 적절한 것이 연결)
가상 함수가 하나라도 들어가게되면, 그 클래스의 크기가 커짐, C등의 다른 언어로 선언된 동일한 자료구조와 호환성도 없어짐
STL 컨테이너 타입들은 전부 가상 소멸자가 없으므로 이를 상속하면 문제 발생
기본 클래스에 가상 소멸자를 주는 규칙은 다형성을 가진 기본 클래스에만 적용됨
- 기본 클래스로 사용할 수 있지만, 다형성은 갖지 않도록 설계된 클래스도 있음(기본 클래스의 인터페이스를 통한 파생 클래스 객체의 조작이 허용되지 않음, Uncopyable 객체)
항목 8
소멸자에서는 예외가 빠져나가면 안됨. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아내야함.
어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 소멸자가 아닌 함수이어야함.
예외를 던지는 소멸자는 곧 ‘걱정거리’를 의미
두가지 회피할 방법이 있음
예외가 발생하면 프로그램 종료 std::abort() 호출
예외를 삼켜버림(무시한 뒤라도 프로그램이 신뢰성 있게 실행 지속 가능해야함)
- 로그 출력
어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야함.
사용자에게 에러를 처리할 수 있는 기회를 주는 것이 좋음
항목 9
생성자 혹은 소멸자 안에서 가상 함수 호출하면 안됨. 지금 실행 중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스의 함수 호출되지 않음.
기본 클래스 생성 과정에서는 가상 함수가 먹히지 않음
기본 클래스의 생성자가 실행되는 동안은 그 객체의 타입은 기본 클래스로 취급됨
파생 클래스가 먼저 소멸되므로, 소멸자도 마찬가지로 기본 클래스로 취급됨
특히 가상 함수를 호출하는 비가상 함수를 소멸자나 생성자에 넣을 경우 디버깅하기 어려움
대처 방법
비가상 멤버 함수로 바꾸는 것
이에 관한 정보를 파생클래스에서 넘겨줘야함
이러한 정보들은 미초기화된 데이터 멤버가 아니어야 하므로, 파생 클래스에서 static 멤버함수로 넘겨주는 것이 좋음
- 도우미 함수로, 기본클래스에 멤버 초기화 리스트가 많이 있을 경우 특히 편리하다고 함
항목 10
대입 연산자는 *this의 참조자를 반환하도록 만들어야함
대입 연산자는 여러 개가 사슬처럼 엮일 수 있는 성질이 있음
대입 연산자는 우측 연관(right associative) 연산이라는 특성이 있음
이러한 구현은 관례(convention)
+=
,-=
,*=
도 마찬가지
항목 11
operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만들어야함
원본 객체와 복사 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 사용해도 됨.
두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인해야함
자기대입(self assignment): 자기 자신에 대해 대입 연산자를 적용하는 것
중복참조(aliasing)로 인해 자기대입이 생김
아래는 안전하지 않은 구현
1
2
3
4
5
6
7
8
9
10
11
class C{
Bitmap* pb;
};
C C::operator=(const C& rhs) {
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
*this
와rhs
가 같은 객체일 가능성이 있다는 것- 같은 객체이면,
delete
가rhs.pb
에도 적용되기 때문
- 같은 객체이면,
따라서 일치성 검사(identity test)를 통해 점검해야함 (전통적인 방법)
- 하지만 예외 안정성이 문제
new Bitmap
에서 예외가 생기면? (메모리 부족 or 복사생성자에서의 예외)
- 하지만 예외 안정성이 문제
1
2
3
4
5
6
C C::operator=(const C& rhs) {
if (this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
아래는 예외 안전한 코드
- 많은 경우에 문장 순서를 바꾸는 것만으로 예외에 안전한 코드가 만들어진다라는 법칙
- 일치성 검사 분기문을 제거할 수 있음
1 2 3 4 5 6 7
C C::operator=(const C& rhs) { Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this; }
아래는 복사 후 맞바꾸기(copy and swap) 기법을 통해 예외 안정성과 자기 대입 안정성을 동시에 가짐(항목 29 참고)
- operator= 작성에 자주 사용함
- 복사 생성자를 사용한 코드는 컴파일러가 더 효율적인 코드를 생성할 수 있는 여지가 있음
1 2 3 4 5 6 7 8 9 10 11 12 13 14
void C::swap(C& rhs); C C::operator=(const C& rhs) { C temp(rhs); swap(temp); return *this; } C C::operator=(C rhs) { swap(rhs); return *this; }
항목 12
객체 복사 함수는 모든 데이터 멤버, 기본 클래스 부분 등을 빠뜨리지 말고 복사해야한다.
클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도를 해서는 안된다. (대신, 공통된 기능을 다른 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결)
객체를 복사하는 함수: 복사 생성자, 복사 대입 연산자
멤버 변수 일부만 복사하는 부분 복사는 좋지 못함
상속의 경우 파생 클래스의 복사 함수에서 기본 클래스 부분 또한 신경써야함
- 각 복사함수의 의미와 복사함수 한쪽에서 다른 쪽을 호출하면 안되는 이유
- 복사 생성자: 객체를 생성
- 즉, 대입 연산자에서 복사 생성자를 호출하는 것은 말이 안되는 발상 (‘생성’의 기능이 필요 없음)
- 대입 연산자: 이미 초기화된 객체에 대해서 값을 주는 것
- 즉, 복사 생성자에서 대입 연산자를 호출하는 것은 아직 초기화도 안된 객체(생성중인 객체)에 대해 대입 연산을 적용하는 것은 말이 안되는 발상
- 복사 생성자: 객체를 생성