52. 생성자/소멸자 내부에서의 가상 함수 호출과 동적 타입의 변화
난이도 상내 리뷰 · C++
해설 · C++
해설 — 생성자/소멸자 내부에서의 가상 함수 호출과 동적 타입의 변화
난이도: 상
요약
객체가 생성/소멸되는 중에는 그 시점의 동적 타입이 "현재 실행 중인 생성자/소멸자가 속한 클래스"로 고정된다. 그래서 Base 생성자 안에서 부른 OnInit()은 Actor를 오버라이드했더라도 Base::OnInit을 호출한다. 소멸자도 대칭으로, Base 소멸자 안의 OnShutdown()은 Base::OnShutdown을 부른다. 생성자와 소멸자에서의 가상 호출은 최종 오버라이더로 가지 않는다.
핵심 개념
- 가상 디스패치는 객체의
vptr가 가리키는 vtable을 통해 이뤄진다. - 생성 순서: 기반(Base) → 파생(Actor). 각 생성자 진입 시 컴파일러가
vptr를 그 클래스의 vtable로 설정한다. 따라서 Base 생성자 실행 중에는 vptr가 Base의 vtable을 가리킨다. - 소멸 순서: 파생 → 기반(생성의 역순). 파생 소멸자 본문이 끝나고 기반 소멸자에 들어갈 때
vptr가 다시 Base vtable로 되돌려진다. - 이 규칙은 "아직 초기화되지 않은(또는 이미 파괴된) 파생 멤버를 가상 함수가 건드리는" 사고를 언어 차원에서 막기 위한 것이다.
단계별 내부 동작 (검증된 실제 출력)
--- construct ---
Base() -> Base::OnInit // Base 생성자 중: 동적 타입 = Base
Actor() // 이후 Actor 생성자 본문(buffer_ 할당 완료)
--- direct call ---
Actor::OnInit (buffer_=0x...) // 완성된 객체에 대한 호출 → 최종 오버라이더
--- destruct ---
~Actor() // delete p: 파생 소멸자 본문 먼저(buffer_ 해제)
~Base() -> Base::OnShutdown // 그 다음 기반 소멸자, vptr=Base → OnShutdown은 Base판
즉 delete p는 ~Actor()(본문·buffer_ 해제) → ~Base()(vptr가 Base로 복귀, OnShutdown은 Base판) 순으로 진행된다.
흔한 오해·함정
- "가상 함수니까 생성자에서 불러도 파생 버전이 실행되겠지" — 아니다. 초기화 순서를 파생 생성자에 의존하는
OnInit을 Base 생성자에서 부르면,Actor::OnInit이 아니라Base::OnInit이 불려 초기화 훅이 무력화된다(버그의 단골). - 순수 가상을 생성/소멸 중 호출하면:
OnInit()이= 0이고 Base 생성자에서 직접 부르면 정의가 없어 정의되지 않은 동작(대개 링크/런타임 "pure virtual call" 오류). 간접(다른 함수 경유) 호출도 동일 위험. - RAII 초기화 훅이 필요하면 생성자에서 가상 호출 대신 2단계 초기화(생성 후 별도
Init()호출)나 팩토리에서 완성 후 훅 호출을 쓴다.
면접 포인트
- 생성/소멸 중 동적 타입이 고정되는 이유와 vptr 재설정 타이밍.
- "생성자에서 가상 함수 호출 금지" 관용구의 근거와 대안(2단계 초기화/팩토리/CRTP).
- 순수 가상 함수를 생성/소멸 중 호출했을 때의 결과.