Item9: 생성자나 소멸자에서 가상 함수를 호출하지 마라.
생성자나 소멸자에서 virtual 함수를 호출하면 예상한 대로 동작하지 않기 때문에 호출하면 안 된다.
객체를 생성하면 기반 클래스의 생성자 -> 파생 클래스의 생성자 순으로 호출된다.
이 때 함수가 기반 클래스의 생성자에 들어가있다면, 파생 클래스의 함수가 아니라 기반 클래스의 함수가 호출된다.
이 개념은 기반 클래스 생성자 실행 후에는 기본 클래스 타입 -> 파생 클래스 생성자 실행 후에는 비로소 파생 클래스 타입이 된다고 생각하면 된다.
소멸자에서도 마찬가지로, 파생 클래스의 소멸자 -> 기반 클래스의 소멸자 순으로 호출되고, 파생 클래스 -> 기반 클래스 순으로 소멸된다.
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const; // 이제 비가상 함수
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // 비가상 호출이므로 안전함
}
class BuyTransaction : public Transaction {
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters)) // 로그 정보를 기본 클래스 생성자에 전달
{ ... }
private:
static std::string createLogString(parameters);
};
따라서 위와 같이 호출하는 방법이 있다.
static으로 함수를 만든다면 기반 클래스 생성 전에 이미 함수가 정의되어 있기 때문에 해당 함수를 거쳐서 기반 함수의 생성자가 실행된다.
Item10: 대입 연산자는 *this에 대한 참조를 반환해야 한다.
대입 연산자는 연속적인 대입이 가능하다.
int x, y, z;
x = y = z = 15; // 대입 연산자 체이닝(chain of assignments)
대입 연산은 우측부터 일어나므로 operator=이 좌측 피연산자에 대한 참조를 반환하는 것을 알 수 있다.
class Widget {
public:
...
Widget& operator=(const Widget& rhs) // 반환 타입은 현재 클래스에 대한 참조
{
...
return *this; // 좌측 객체를 반환
}
};
따라서 대입연산자를 직접만들 때 *this를 반환해야 한다.
this는 Widget*이므로 *this는 Widget 좌측 객체 반환
Item11: operator= 에서 자기 자신에 대한 대입을 처리하라.
class Widget { ... };
Widget w;
...
w = w; // 자기 자신에 대한 대입
어색해보이지만 이러한 작업이 일어날 수도 있다는 염두를 해야 한다.
a[i] = a[j]; // 자기 자신에 대한 대입 가능성 있음
i와 j가 동일할 때 자기 자신에 대한 대입이 일어난다.
동일한 타입이 아니더라도 같은 객체일 수 있다.
예를 들어 Base Class포인터가 Derived Class 객체를 가리킬 수 있다.
기본적으로는 큰 문제가 없을 수 있지만,
만약에 동적 리소스 관리 시에 operator =에서 사용중이던 객체를 delete를 하고 파라미터로 받은 참조를 넣어준다고 가정하자.
이 때 원 객체를 delete하고 exception이 일어난다면, 오류가 발생할 수 있다.
해결법
예외 안전성을 보장하면 자기 자신에 대한 대입또한 안전하게 할 수 있다.
copy-and-swap 기법을 통해 안전성을 보장한다.
class Widget {
public:
void swap(Widget& rhs); // *this와 rhs의 데이터를 교환
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // rhs의 복사본 생성
swap(temp); // *this와 temp의 데이터 교환
return *this;
}
C++11이후에 생긴 이동 연산자 (move assignment operator)를 통해서 더 간단히 해결할 수도 있을 것 같다.
(rvalue참조 개념)
Item12: 객체의 모든 부분을 복사하라.
잘 설계된 객체지향 시스템의 경우 복사 생성자, 복사 대입 연산자를 통해서만 객체를 복사해야 한다.
기본적으로 만들어주지만, 직접 생성한다면 이는 컴파일러에게 기본 구현이 마음에 들지 않는다고 하는 것이다.
그래서 직접 만들면 구현이 잘못되었더라도, 그 사실을 알려주지 않는다.
만약에 클래스에 새로운 멤버가 추가되거나, 상속을 받고 있다면 추가 조치를 하지 않는다면 오류가 생길 수 있다.
새로운 멤버가 생기면 복사 시에 해당 멤버를 복사할 수 있도록 추가하자.
상속을 받고 있다면 기반 클래스의 복사를 고려해야 한다.
끝!
'C, C++ > Effective C++' 카테고리의 다른 글
| 4장 설계와 선언 (0) | 2025.03.21 |
|---|---|
| 3장 리소스 관리 (0) | 2025.03.21 |
| 2장 생성자, 소멸자 그리고 할당 연산자 (1) (0) | 2025.03.16 |
| 1장 C++과 친해지기 (2) (0) | 2025.03.15 |
| 1장 C++과 친해지기 (1) (0) | 2025.03.14 |