본문 바로가기

Effective C++/2. 생성자, 소멸자 및 대입 연산자

항목 9: 객체 생성 및 소멸 과정 중에는 절대로 가상함수를 호출하지 말자

주식 거래를 본떠 만든 클래스 계통 구조가 있다고 하자. 매도, 매수 등등이 있어야 것인데 이러한 거래를 모델링할 감사(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:: 붙여줘야 해서, 실수로 데이터에 접근할려고 하면 에러를 띄워줌)

 

!! 미초기화된 데이터 맴버는 정의되지 않은 상태(메모리공간이 생기기 전이다.) 있다 !!

 

  • 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당하는 클래스의 파생 클래스 쪽으로는 내려가지 않으니까요.