Character* Ptr만 보면 컴파일러가 아는 것은 Character*뿐인데, 런타임에는 어떻게 Player::Attack을 찾아갈까?
대부분의 C++ 구현은 이를 위해 vptr + vTable 구조를 사용한다.
vptr(Virtual Table Pointer)는 객체가 자신의 vTable을 찾기 위해 가지는 숨겨진 포인터다.
class Character
{
public:
virtual void Attack();
virtual void Move();
private:
int Health;
};
그래서 virtual 함수가 하나라도 있으면 객체 크기가 커진다.
class A // sizeof == 4
{
int Value;
};
class B // sizeof == 16
{
public:
virtual void Func();
int Value;
};
B는 vptr 8Byte + int 4Byte + 정렬 패딩 4Byte로 16Byte가 된다.
vTable은 클래스의 virtual 함수 주소를 담은 테이블이며, 객체마다가 아니라 클래스마다 하나 존재하고 그 클래스의 모든 객체가 공유한다.

덕분에 객체는 함수 주소를 전부 들고 다닐 필요 없이 vptr 하나만 가지면 된다.
Character에 이어서 Player 자식 클래스를 선언하게 되면 다음과 같이 vTable이 선언된다.
class Player : public Character
{
public:
void Attack() override; // Move는 Override하지 않음
};

Override한 슬롯만 Derived 함수로 교체되고, 나머지는 Base 구현을 그대로 유지한다. Player 객체의 vptr은 Player용 vTable을 가리킨다.
만약 다음과 같이 코드가 작성되어 있다면 vPtr은 실제 메모리에 할당된 주소를 기반으로 선언이 되므로, 이 경우 Player의 vTable을 가리키게 된다.
Character* Ptr = new Player();
Ptr->Attack();

순수 가상 함수도 vTable에서 슬롯을 차지하지만 그 자리에는 호출 가능한 최종 구현이 없는데,
만약 Character::Attack이 순수 가상함수라면 이 자리에 _purecall 같은 런타임 핸들러가 포함이 된다.

순수 가상 함수를 직접적으로 호출하는 것을 불가능 하지만, 생성자를 통해서 호출하는 등 우회 가능성이 있으므로 이러한 핸들러가 포함된 것으로 보인다.
RTTI(Run-Time Type Information)는 실행 중 객체의 실제 타입 정보를 확인하기 위한 기능이며, typeid와 dynamic_cast가 이를 사용한다.
#include <typeinfo>
Player MyPlayer;
Character* Ptr = &MyPlayer;
std::cout << typeid(*Ptr).name(); // class Player
앞서 살펴보았던 구조에서 Character가 virtual 함수를 하나 이상 가진 타입임을 확인했으므로 *Ptr의 동적 타입을 확인할 수 있다.
만약 가상함수가 하나라도 없다면 typeid는 런타임 정보를 보지 않고 정적 타입을 그대로 알려준다.(class Character)
dynamic_cast는 클래스 계층에서의 타입 변환이 실제 객체 구조상 유효한지를 런타임에 검사하는 Cast다.
class Character
{
public:
virtual ~Character() = default;
};
class Player : public Character
{
public:
void PlayerOnlyFunction() {}
};
class Enemy : public Character {};
Player MyPlayer;
Character* CharacterPtr = &MyPlayer;
Player* PlayerPtr = dynamic_cast<Player*>(CharacterPtr); // 성공
Enemy* EnemyPtr = dynamic_cast<Enemy*>(CharacterPtr); // 실패 → nullptr
실패 방식은 포인터와 참조가 다르다.
try
{
Enemy& EnemyRef = dynamic_cast<Enemy&>(*CharacterPtr);
}
catch (const std::bad_cast&)
{
std::cout << "Cast Failed\n";
}
dynamic_cast 포인터 실패 → nullptr
dynamic_cast 참조 실패 → std::bad_cast 예외
class Character {}; // virtual 함수 없음
class Player : public Character {};
Character* CharacterPtr = new Player();
Player* PlayerPtr = dynamic_cast<Player*>(CharacterPtr); // 컴파일 에러
런타임 검사가 필요한 Downcast는 Source Type이 virtual이 하나 이상 있어야 한다.
그래야 typeid가 생성되어 dynamic_cast를 수행할 수 있기 때문이다.
Derived → Base 방향의 Upcast는 상속 관계가 컴파일 시점에 확정되어 있으므로 런타임 타입 검색이 필요 없다.
Player* PlayerPtr = new Player();
Character* CharacterPtr = dynamic_cast<Character*>(PlayerPtr);
static_cast
컴파일 시점의 타입 관계만 사용
Downcast 시 런타임 검증 없음 -> 캐스팅은 되지만 올바른 검증인지는 불확실
dynamic_cast
실제 Dynamic Type 확인
Pointer 실패 → nullptr / Reference 실패 → std::bad_cast
런타임 비용 발생, virtual 1개 이상 필요
class Character
{
public:
Character()
{
Initialize();
}
virtual void Initialize()
{
std::cout << "Character Initialize\n";
}
};
class Player : public Character
{
public:
void Initialize() override
{
std::cout << "Player Initialize\n";
}
};
Player MyPlayer;
Player::Initialize가 실행될 것 같지만, 실제 출력은 다음과 같다.
Character Initialize
생성자와 소멸자가 실행되는 동안의 virtual 호출은 현재 생성/소멸 중인 클래스보다 더 파생된 클래스의 Override로 내려가지 않는다.
그 이유는 생성 순서를 보면 이유가 분명해진다.
Player 생성 시작
↓ Character 생성
↓ Character 생성자 호출
↓ Player의 필드 초기화
↓ Player 생성자 호출
Player 생성 완료
Character 생성자가 도는 시점에는 Player 부분이 아직 초기화되지 않았기 때문이다.