class Empty
{
public:
// 기본 생성자
Empty() { ... } // 생성자를 하나라도 만들면 컴파일러는 기본 생성자를 선언하지 않음
// 소멸자
~Empty() { ... }
// 복사 생성자
Empty(const Empty& rhs) { ... }
// 복사 대입 연산자
Empty& operator=(const Empty& rhs) { ... }
};
참조자를 데이터 멤버로 갖고 있는 클래스에 대입 연산을 지원하기 위해서는 직접 복사 대입 연산자를 정의해 주어야 함
template<class T>
class NamedObject
{
public:
NamedObject(std::string& name, cosnt T& value);
private:
std::string& nameValue;
const T objectValue;
};
int main()
{
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s; // 컴파일 에러
}
데이터 멤버가 상수 객체인 경우에도 C++ 컴파일러가 비슷하게 동작함
상수 멤버를 수정하는 것은 문법에 어긋나기 때문에 자동으로 만들어진 복사 대입 연산자 내부에서는 상수 멤버를 어떻게 처리해야할지 모호해짐
컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면 대응되는 멤버 함수를 private
으로 선언한 후에 구현은 하지 않은 채로 두면 됨
하지만 이 경우에는 friend
나 멤버 함수
를 통해서는 접근이 가능하기 때문에 delete
를 사용하여 함수를 삭제하는 것이 좋음
소멸자는 파생 클래스 -> 기본 클래스 순으로 호출됨
기본 클래스를 사용하여 파생 클래스의 메모리를 해제하는 경우 virtual
키워드를 사용해야만 파생 클래스의 소멸자부터 호출됨
이렇게 하지 않으면 파생 클래스의 힙 영역에 저장된 메모리가 있는 경우 제대로 해제가 되지 않아 누수가 발생될 수 있음
가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조(vptr
)가 하나 들어가야 함
vptr
은 가상 함수의 주소, 즉 포인터들의 배열을 가리키고 있으며 가상 함수 테이블 포인터의 배열은 vtbl
이라고 불림
vptr
이 추가 되면 객체의 크기가 커지기 때문에 모든 객체의 소멸자를 virtual
로 선언해서는 안 됨
가상 소멸자를 선언하는 것은 그 클래스에 가상 함수가 하나라도 들어 있는 경우로 한정해야 함
경우에 따라 순수 가상 소멸자를 두면 편리하게 사용할 수 있음
순수 가상 함수는 해당 클래스를 추상 클래스로 만드는데 마땅히 넣을 만한 순수 가상 함수가 없는 경우가 종종 생김
이럴 때 순수 가상 소멸자를 선언하면 됨
소멸자에서는 예외가 빠져나가면 안 됨
만약 소멸자 안에서 호출되 함수가 예외를 던질 가능성이 있다면 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리던지 프로그램을 끝내든지 해야 함
어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 함
기본 클래스의 생성자가 호출될 동안에는 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않음
그 대신 객체 자신이 기본 클래스 타입인 것처럼 동작함
기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아님
어쩌다 호출된 가상 함수가 파생 클래스 쪽으로 내려간다면 파생 클래스 버전의 가상 함수는 초기화 되지 않은 데이터 멤버를 건드리게 되므로 미정의 동작이 이루어짐
int x, y, z;
x = y = z = 15;
대입이 사슬처럼 이루어지는 이유는 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있기 때문임
단순히 대입형 연산자 말고도 모든 형태의 대입 연산자에서 지켜져야 함
관례이기 때문에 지키지 않는다고 컴파일이 안 된다거나 하지는 않지만 모든 기본제공 타입들이 따르고 있을 뿐만 아니라 표준 라이브러리에 속한 모든 타입에서도 따르고 있으므로 지키는 것이 좋음
어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것
같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려하는 것이 일반적으로 바람직한 자세임
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap& pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
이렇게 코드를 짜면 new Bitmap
부분에서 예외가 발생하더라도 pb
는 변경되지 않은 상태로 유지되기 때문에 안전함
또한 일치성 검사 같은 것이 없음에도 불구하고 이 코드는 자기대입 현상을 완벽히 처리하고 있음
객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 함
클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 하지 말고 공통된 동작을 제3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결해야 함