Fundamental of CS/: : C++

[Inheritance] Virtual Table

Jay.P Morgan 2024. 7. 16. 11:03

 

 

Virtual Function과 Virtual Table(vtable)의 동작 원리 정리

이번 시간에는 C++에서 Dynamic Polymorphism(동적 다형성)을 구현하는 핵심 기술인 Virtual Function과 Virtual Table(vtable)의 동작 원리에 대해 알아보았습니다.


1. Virtual Function 사용 시 객체 크기의 변화

기본적인 상속 구조에서는 클래스가 가진 멤버 변수의 크기만큼 객체 크기가 결정됩니다.

예를 들어:

class Animal
{
    double height;
};

class Cat : public Animal
{
    double weight;
};

이라면:

  • Animal
    • double 1개 → 8Byte
  • Cat
    • Animal의 height
    • Cat의 weight
    • double 2개 → 16Byte

가 됩니다.


하지만 Virtual Function을 추가하면 객체 크기가 증가합니다.

virtual void Speak();

를 추가하면:

  • Animal: 8Byte → 16Byte
  • Cat: 16Byte → 24Byte

로 증가합니다.


증가하는 이유: vptr 추가

Virtual Function을 사용하면 컴파일러는 객체 내부에 추가적인 포인터 정보를 삽입합니다.

이 포인터를:

vptr (Virtual Pointer)

이라고 합니다.

64bit 시스템에서는 포인터 크기가:

8Byte

이므로 객체마다 8Byte가 추가됩니다.

결과:

Animal

데이터크기

vptr 8Byte
height 8Byte
합계 16Byte

Cat

데이터크기

vptr 8Byte
height 8Byte
weight 8Byte
합계 24Byte

2. Virtual Table(vtable)의 구조

vptr은 특정 테이블을 가리키는데, 이것이:

Virtual Table(vtable)

입니다.

클래스마다 각각의 vtable이 생성됩니다.

예:

Animal vtable
----------------
Animal::Speak()
Animal::~Animal()


Cat vtable
----------------
Cat::Speak()
Cat::~Cat()

즉:

  • Animal 객체 → Animal vtable 가리킴
  • Cat 객체 → Cat vtable 가리킴

이라는 구조가 됩니다.


3. Dynamic Polymorphism 동작 과정

다음 코드를 생각해 볼 수 있습니다.

Animal* ptr = new Cat();

ptr->Speak();

겉으로 보면:

  • 포인터 타입: Animal*
  • 생성된 객체: Cat

입니다.


메모리 구조:

Stack

ptr
 |
 |
 v

Heap

+----------------+
| vptr ----------|-----> Cat vtable
| height         |
| weight         |
+----------------+

ptr->Speak() 호출 과정:

1단계

포인터가 가리키는 객체 확인

ptr → Cat 객체

2단계

객체 내부의 vptr 확인

vptr → Cat vtable

3단계

vtable에서 함수 주소 확인

Cat vtable
 |
 └── Cat::Speak()

4단계

Cat의 Speak 실행

출력:

Meow

즉 함수 호출이 컴파일 시간에 결정되는 것이 아니라:

실행 시간(Runtime)에 실제 객체 타입을 확인하고 호출할 함수를 결정

하게 됩니다.

이것이 Dynamic Polymorphism입니다.


4. Animal 객체를 생성하는 경우

만약:

Animal* ptr = new Animal();

이라면:

ptr
 |
 v

Animal 객체

vptr → Animal vtable

구조가 됩니다.

따라서:

ptr->Speak();

호출 시:

Animal::Speak()

가 실행됩니다.


5. Virtual Destructor와 vtable

이전 시간에서:

Base 클래스의 Destructor는 virtual로 선언해야 한다

라고 배웠습니다.

이유는 Destructor 역시 Virtual Function이기 때문입니다.

예:

virtual ~Animal();

을 추가하면:

Animal vtable:

Animal vtable
----------------
Animal::Speak()
Animal::~Animal()

Cat vtable:

Cat vtable
----------------
Cat::Speak()
Cat::~Cat()

형태가 됩니다.


따라서:

Animal* ptr = new Cat();

delete ptr;

를 실행하면:

  1. Cat Destructor 호출
  2. Animal Destructor 호출

순서로 정상적으로 객체가 제거됩니다.


핵심 정리

개념설명

Virtual Function 런타임 함수 선택을 가능하게 하는 함수
vptr 객체 내부에 추가되는 Virtual Table 포인터
vtable Virtual 함수 주소를 저장하는 테이블
Dynamic Polymorphism 실행 중 실제 객체 타입에 따라 함수 호출
Virtual Destructor Base 포인터로 Derived 객체 삭제 시 필수

전체 동작 흐름

Animal* ptr = new Cat();

        ptr
         |
         v

     Cat Object
     +-------------+
     | vptr        |
     | height      |
     | weight      |
     +-------------+
         |
         v

     Cat vtable
     +-------------+
     | Cat::Speak  |
     | Cat::~Cat   |
     +-------------+

결론적으로 C++의 Dynamic Polymorphism은 마법처럼 동작하는 것이 아니라,

  1. 객체 내부의 vptr
  2. 클래스별 vtable
  3. vtable 안의 함수 주소

를 이용해서 런타임에 호출할 함수를 결정하는 구조입니다.

이 원리를 이해하면 이후에 배우게 되는 Object Slicing, Dynamic Cast, Multiple Inheritance, ABI 구조 등의 고급 C++ 개념을 이해하는 기반이 됩니다.