C++의 클래스 멤버함수 중에서 복사 생성자, 복사 대입 연산자, 소멸자는 사용자가 직접 선언하지 않을 경우 필요할 때 자동으로 public inline으로 기본형이 삽입된다.
컴파일러가 만들어주는 복사생성자와 복사대입연산자는 destination의 각 멤버함수의 생성자에 source의 멤버함수를 넘기는 식으로 이루어진다.
그런데 참조자나 상수 객체를 멤버로 가질 경우 이러한 방식으로 복사대입연산자가 구현되면 적법하지 않으므로, 이 때엔 직접 복사 대입 연산자를 정의해야 한다.
복사 생성자와 복사 대입 연산자는 사용자가 선언하지 않을 때에도 컴파일러가 자동으로 만들어내는 대표적인 두 함수이다.
이 둘의 사용을 금지하기 위해 반드시 private으로 선언하자.
이 때 매개변수 이름은 생략해도 되고, 이렇게 선언된 기본 클래스를 만들어 상속하도록 활용할 수도 있다.
C++ 의 규정에 따르면, 기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될 때, 소멸자가 비 가상으로 선언되어 있다면 미정의 동작이 발생한다.
이를 막기 위해 언제나 기본 클래스에는 소멸자를 가상으로 선언한다면, 어떤 포인터로 객체를 삭제하더라도 메모리 누수없이 객체가 완벽하게 소멸될 것이다.
그러나, 파생될 가능성이 없는 즉 기본 클래스로 사용하지 않을 클래스의 소멸자를 가상으로 선언하는 것은 좋지 않다.
왜냐하면 클래스에 가상 함수가 포함되는 순간 객체에 가상 함수 테이블(vptr)을 포함하게 되어 크기가 커지기 때문이다.
이는 호환성과 이식성을 깨트리게 되어 중대한 문제를 발생시킬 수도 있다.
또 주의할 점은, std::string은 비가상 소멸자를 가지고 있으므로 이것의 파생 클래스를 만드는 일은 없어야 한다.
std 컨테이너 타입 (vector list set 등) 모두가 비가상 소멸자를 가지고 있으므로 주의하자.
C#에는 sealed, java에는 final 이라는 파생을 금지시키는 키워드가 있지만 C++에는 존재하지 않는다.
예외를 내보내는 소멸자는 밖에서 좋지않은 결과, 미정의 동작을 발생시킬 확률이 있다.
만약, 데이터베이스 연결과 같은 자원 객체의 소멸자에서 예외가 발생할 가능성이 있다면, 자원 관리 클래스를 사용할 수 있다.
이 자원 관리 클래스는 3장에서 자세하게 다뤄지며, 자원 관리 클래스의 소멸자에서 자원의 소멸을 담당하도록 만드는 것이다.
그러나 이 자원 관리 클래스의 소멸자에서 예외가 발생할 수 있다.
이 때, 두가지 선택지가 있다.
예외 발생시 std::abort 호출로 프로그램을 바로 끝내버린다.
예외를 삼켜 로그를 출력한다. : 왜 예외가 발생했는지 정보가 사라져버린다.
둘 다 문제점이 있으므로, 사용자에게 Close 라는, 소멸자가 아닌 자원 소멸 함수를 제공함으로써 안전장치를 추가할 수 있다.
사용자는 이 Close 함수를 호출하여 직접 예외를 처리할 수 있는 기회를 갖게 된다.
중요한 것은, 소멸자에서 책임감없이 예외를 내뿜어서는 안된다는 점이다.
파생 클래스의 생성 혹은 소멸의 과정을 떠올려보자.
생성은 기본 클래스에서 파생 클래스의 순서로, 소멸은 파생 클래스에서 기본 클래스의 순서로 진행된다.
기본 클래스의 생성자 마지막에 선언된 가상 함수 호출은 절대 파생 클래스 쪽으로 전파되어 호출되지 않는다.
기본 클래스의 생성자 호출 중에는 파생 클래스의 데이터 멤버가 초기화된 상태가 아니기 때문이다.
혹, 파생 클래스의 함수가 호출된다 하더라도 커다란 문제가 생길 가능성이 다분하므로, 그냥 C++은 애초에 이런식으로 사용하지 않도록 설계되었다.
만약 기본 생성자에서 파생 클래스의 정보가 반드시 필요한 경우엔 다음과 같이 파생 클래스의 생성자가 기본 클래스의 생성자로 정보를 올려보내 주도록 한다.
class Transaction(
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // 비가상 함수임에 주목
};
Transaction::Transaction(const std::string& logInfo)
{
logTransactio(logInfo);
}
class BuyTransaction: public Transaction {
public:
BuyTransaction( parameters ) :
Transaction(createLogString(parameters))
{
...
}
private:
static std::string createLogString( parameters );
};
대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 하는 것이 관례이므로, 사용자 정의 클래스 또한 이러한 규칙을 지킬 수 있도록 구현하자.
이는 단순 대입이 아닌 += 과 같은 형태의 대입 연산자에서도 마찬가지이다.
Widget& oprater=(const Widget& rhs)
{
return *this;
}
표준 라이브러리들 또한 지키고 있는 관례이므로 반드시 지킬 수 있도록 하자!
자기대입이란 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것이다.
Widget w;
w = w;
별 문제가 없어보이지만 문제가 있다는 게 가장 큰 문제이다.
대입 연산자 내에서 어떤 동작을 할지 모르기 때문이다.
따라서 대입 연산자에서는 반드시 일치성 검사를 넣어 다음과 같이 구현해야 한다.
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
...
}
혹은 예외 안정성을 높인 다음과 같은 방법도 있다
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrginal = pb;
pb = new Bitmap(*rhs.pb);
delete pOriginal;
return *this;
}
원래의 pb를 기억해두고 새 객체를 생성한 다음 원래의 pb를 삭제하는 방법이다.
이러한 자기대입 연산은 자주 일어나는 일이 아니므로, 일치성 검사보다 위 방법이 나을 수 있다.
세번째 방법으로 copy and swap이 있다. 자세한 사항은 항목 29 참조.
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp); // *this와 temp의 데이터를 맞바꾼다.
return *this;
}
복사 생성자와 복사 대입 연산자와 같이 복사 함수를 직접 구현할 때에는 객체의 모든 부분을 빠짐없이 복사하고 있는지 항상 주의해야 한다.
구현하는 도중 새로운 멤버가 추가되는 일도 부지기수니, 그럴때도 놓치지 않도록 하자.
상속된 클래스에서도 기본 클래스의 복사 함수를 호출해 줌으로써 모든 멤버를 복사할 수 있도록 유의하자.
단, 복사 생성자와 복사 대입 연산자는 서로를 호출해서는 절대 안된다. 차라리 공통된 동작을 제 3의 함수에 구현해서 양쪽이 각각 이 함수를 호출하도록 하는 편이 좋다.