주식 거래를 본떠 만든 클래스 계통 구조가 있다고 하자. 매도, 매수 등등이 있어야 할 것인데 이러한 거래를 모델링할 때 꼭 감사(audit)기능이 있어야 한다. 그래서 어떤 주식 거래 객체가 생성될 때 마다 감사 로그에 적
절한 거래 내역이 나오게 하고 싶다. 그래서 다음과 같이 코드를 짰음.
BuyTransaction의 생성자가 호출될 탠데, 생성자의 호출 순서는 base 클래스의 생성자가 먼저 호출되고 그 다음 derived 생성자가 호출되고 … 순서이다.
base 클래스 생성자에 가상함수인 logTransaction을 호출하는 부분이 보이는데, 이때의 logTransaction은 BuyTransaction가 아니라 Transaction의 것이다. 기본 클래스의 생성자가 호출될 동안에는 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다. (overriding X)
기본 클래스 생성자가 돌아가고 있을 시점에는 파생 클래스 데이터 맴버는 아직 초기화된 상태가 아니기 때문이다.(파생 클래스의 생성자가 호출되기 전이기 때문이다.)
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안, 그 객체의 타입은 바로 기본 클래스 이다.
b가 생성될 때, Transaction() 이 호출되는 동안 b의 type은 Transaction이라는 것이다. 그래서 Transaction() 안에서 호출되는 가상함수들은 전부 Transaction의 것으로 결정되고, 만약 런타임 타입 정보를 사용하는 언어 요소(typeid, dynamic_cast) 등을 사용해 b의 타입을 살펴보면 Transaction이라고 뜸
BuyTransaction 클래스만의 데이터가 초기화 되기 전이기 때문에 아예 없었던 것처럼 취급함. BuyTransaction의 생성자가 호출될 때 비로소 BuyTransaction이라고 취급해준다.
객체가 소멸될 때(소멸자가 호출될 때)에도 똑같이 생각하면 된다.
소멸자의 호출 순서는 derived 클래스의 소멸자 -> base 클래스의 소멸자
derived 클래스의 소멸자가 호출되면 derived 클래스 부분은 없다고 본다. 그래서 base 클래스의 소멸자가 호출될 때 객체는 base 클래스 객체가 되며 가상함수는 전부 base 클래스의 것을 가르킨다.
예제 코드를 다시 보면, Transaction 생성자에서 가상 함수를 호출하는게 바로 눈에 보인다. 이런건 눈에 너무 잘띄기 때문에 경고 메세지를 내어 주는 컴파일러도 있다.
컴파일러 경고까지 안와도 프로그램 실행 전에 문제가 들어난다. logTransaction이 순수가상함수로 선언되어 있기 때문에 정의되지 않은 함수이기 때문에 링커 에러가 난다.
이런 일이 없을 거 같지만, 항목 32를 보면 가능함.
생성자 혹은 소멸자가 여러 개 된다면 골치가 아파진다. 이 안에 가상함수 호출을 찾아내는 일이 생각보다 어렵다. 생성자들이 하는일이 거의 비슷할 것인데, 똑같은 작업을 모아 공통된 초기화 코드 init()을 만들어놓으면 코드 중복 현상을 막을 수 있다.
대개 이런 설계로 private 맴버인 비가상 초기화 함수가 만들어 지는데 예제를 보자.
logTransaction은 Transaction 클래스 내의 순수 가상 함수 => 대부분의 시스템에서 순수 가상 함수가 호출되면 프로그램을 abort 시킨다.
logTransaction이 그냥 가상 함수이고, Transaction 내에 구현이 되어있다면 머리가 터지기 시작한다.
BuyTransaction b 선언 => Transaction 생성자 호출 => Transaction 버전 logTransaction 호출 => 컴파일, 링커, 런타임 상에서 에러가 나지 않는다. logical Error 임
일단 logTransaction을 비가상함수로 선언함
그리고 필요한 초기화 정보를 derived 클래스 에서 기본 클래스 생성자로 올려주도록 만듬
BuyTransaction 생성자의 맴버 초기화 리스트를 보면 Transaction의 생성자를 호출하는 것이 있는데, 이는 Transaction(base 클래스)의 초기화를 명시적으로 해주는 부분임
즉 Base -> Derived 로 가는 생성자 호출 흐름은 여전한데, 저렇게 명시적으로 적어줌으로써 Transaction()이 아닌 Transaction(logInfo)이 호출되는 것이다.
그리고 createLogString()은 정적 함수로 되있음, 이 함수는 기본 클래스 생성자 쪽으로 넘길 값을 정하는 용도이다. => 다른 데이터(BuyTransaction의 초기화되지 않은 데이터 맴버들, Transaction 생성자 호출 시점이므로 초기화 안됨)들을 건들지 않게 하기 위해 static으로 함( static으로 하면 this가 없어져 다른 데이터로 접근할때 따로 BuyTransaction:: 을 붙여줘야 해서, 실수로 데이터에 접근할려고 하면 에러를 띄워줌)
!! 미초기화된 데이터 맴버는 정의되지 않은 상태(메모리공간이 생기기 전이다.)에 있다 !!
- 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당하는 클래스의 파생 클래스 쪽으로는 내려가지 않으니까요.
'Effective C++ > 2. 생성자, 소멸자 및 대입 연산자' 카테고리의 다른 글
항목 11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2021.04.24 |
---|---|
항목 10: 대입 연자는 *this의 참조자를 반환하게 하자 (0) | 2021.04.24 |
항목 8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2021.04.24 |
항목 7: 다형성을 가진 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2021.04.24 |
항목 6: 컴파일러가 만들어낸 함수가 필요없으면 확실히 이들의 사용을 금해 버리자. (0) | 2021.04.24 |