포스트

[C++] 가상함수와 vtable

[C++] 가상함수와 vtable

C++ 가상 함수(Virtual Function)와 vtable의 동작 원리

C++에서 가상 함수(Virtual Function)상속과 다형성(polymorphism) 을 구현하는 핵심 개념입니다. 가상 함수를 통해 부모 클래스 포인터(또는 참조)를 통해 자식 클래스의 메서드를 호출할 수 있습니다. 이 기능을 가능하게 하는 구조가 바로 vtable(virtual table) 입니다.


1. 가상 함수(Virtual Function)란?

정의

  • 가상 함수는 virtual 키워드로 선언된 멤버 함수입니다.
  • 기본 클래스 포인터를 통해 파생 클래스의 메서드를 호출할 수 있도록 허용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
    virtual void speak() {
        std::cout << "Base speaking\n";
    }
};

class Derived : public Base {
public:
    void speak() override {
        std::cout << "Derived speaking\n";
    }
};

int main() {
    Base* p = new Derived();
    p->speak();  // Derived::speak() 호출됨
    delete p;
}

출력 결과:

1
Derived speaking

2. virtual vs non-virtual 함수

항목virtual 함수일반 함수
바인딩 시점런타임컴파일 타임
호출 방식vtable 간접 참조직접 호출
성능간접 호출로 약간 느림빠름
유연성높음 (다형성 지원)낮음

3. vtable (Virtual Table)이란?

개요

  • 가상 함수 호출을 구현하기 위해 컴파일러가 자동 생성하는 함수 포인터 테이블
  • 클래스마다 하나씩 생성됨
  • 가상 함수가 하나라도 있다면 해당 클래스에 vtable이 생성됨

vptr (Virtual Pointer)

  • 객체 내부에 숨겨진 포인터
  • 이 객체가 사용할 vtable을 가리킴
1
2
3
4
5
객체 구조:
┌─────────────┐
│   vptr      │ → vtable(Class)
│ 멤버 변수들 │
└─────────────┘

4. vtable 동작 원리

객체 생성 시

  • 컴파일러는 객체에 vptr을 삽입
  • vptr은 해당 클래스의 vtable 주소를 저장

함수 호출 시

  • vptr을 통해 vtable에 접근
  • 해당 함수의 포인터를 찾아 호출 → 동적 바인딩 수행
1
2
Base* p = new Derived();
p->speak();  // vptr → vtable(Derived) → &Derived::speak 호출

5. 예시로 보는 vtable 구조

1
2
3
4
5
6
7
8
9
class Base {
public:
    virtual void f() { std::cout << "Base::f\n"; }
};

class Derived : public Base {
public:
    void f() override { std::cout << "Derived::f\n"; }
};

vtable 구조:

1
2
3
4
5
6
7
8
9
vtable(Base):
+----------------+
| &Base::f       |
+----------------+

vtable(Derived):
+----------------+
| &Derived::f    |
+----------------+

호출 흐름:

  • Base* p = new Derived();
  • p->f(); → p → vptr → vtable(Derived) → &Derived::f 호출

6. 생성자/소멸자에서 virtual 함수 호출이 위험한 이유

생성자

  • 객체 생성 도중에는 vptr이 아직 base 클래스의 vtable을 가리킴
  • 이 시점에 virtual 함수를 호출하면 base 버전만 호출됨

소멸자

  • 파생 클래스가 먼저 소멸되므로 vptr은 base 클래스의 vtable로 돌아감
  • 이 시점에 virtual 함수 호출 시 역시 base 버전만 호출됨

예제

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
    Base() { speak(); } // Base::speak 호출됨
    virtual void speak() { std::cout << "Base\n"; }
    virtual ~Base() { speak(); } // Base::speak 호출됨
};

class Derived : public Base {
public:
    void speak() override { std::cout << "Derived\n"; }
    ~Derived() { std::cout << "Derived destroyed\n"; }
};

출력 결과

1
2
3
Base
Derived destroyed
Base

7. 순수 가상 함수와 추상 클래스

순수 가상 함수

1
2
3
4
class Shape {
public:
    virtual void draw() = 0;  // 순수 가상 함수
};
  • 구현이 없는 virtual 함수
  • 해당 클래스를 추상 클래스로 만들며, 인스턴스를 생성할 수 없음
  • 파생 클래스는 반드시 오버라이딩해야 함

vtable 구조

  • 순수 가상 함수도 vtable에 등록됨 (대개 null 또는 특수 표기)

8. 기타 특징 및 주의사항

항목설명
클래스마다 vtable 1개객체당 vptr 1개가 자신의 클래스의 vtable을 가리킴
다중 상속클래스마다 여러 개의 vptr과 vtable이 생길 수 있음
RTTI, dynamic_cast내부적으로 vtable 사용 (typeid도 포함)
성능간접 호출이므로 일반 함수보다 약간 느림
디버깅가상 함수 호출 오류는 런타임 시점에 드러남

9. 요약

개념설명
가상 함수virtual 키워드로 선언, 런타임에 결정됨 (동적 바인딩)
vtable클래스마다 생성되는 함수 포인터 테이블
vptr객체 내부 포인터, 해당 클래스의 vtable을 가리킴
생성자/소멸자이 시점엔 파생 클래스의 vtable을 사용할 수 없음
추상 클래스순수 가상 함수를 포함한 클래스, 직접 인스턴스 생성 불가

가상 함수와 vtable 구조는 객체지향 설계에서 다형성을 구현하는 핵심적인 기반이며, 올바르게 이해하고 사용하면 C++의 유연함과 강력함을 최대한 활용할 수 있습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.