[C++] VTable과 다형성

개발자 김선호·2025년 8월 27일

Virtual Table(가상 함수 테이블, Vtable)은 C++에서 다형성(Polymorphism)을 구현하기 위해 컴파일러가 내부적으로 생성하는 함수 포인터 테이블입니다. 가상 함수가 있는 클래스마다 하나씩 생성되며, 런타임에 올바른 함수를 호출하기 위한 핵심 메커니즘입니다.

주요 정의 및 개념

Vtable의 구성 요소

  • 함수 포인터 배열: 각 가상 함수에 대한 포인터들의 집합
  • Vptr (Virtual Pointer): 각 객체가 가지는 Vtable을 가리키는 포인터
  • 타입별 고유성: 각 클래스마다 고유한 Vtable 보유

장점

  • 다형성 구현: Vtable을 통한 런타임 함수 디스패치
  • 타입 안전성: 컴파일 타임에 Vtable 구조 검증
  • 확장성: 상속 계층에서 Vtable 엔트리 오버라이드로 기능 확장
  • 안전한 소멸: 가상 소멸자를 통한 올바른 리소스 해제

단점

  • 메모리 오버헤드:
    • 객체당 Vptr 추가 (보통 8바이트)
    • 클래스당 Vtable 메모리 사용
    • 그러나 안전한 동작을 위해 필수불가결
  • 간접 호출: Vptr → Vtable → 함수 주소의 이중 간접 참조
  • 캐시 미스: Vtable 접근 시 메모리 지연 가능

Vtable 생성 시점

1. 컴파일 타임 생성

  • 언제: 컴파일러가 소스코드를 분석하는 단계
  • 조건: 클래스에 하나 이상의 가상 함수가 선언된 경우
  • 위치: 일반적으로 첫 번째 가상 함수가 정의된 번역 단위에 생성

2. 링킹 시점에서의 병합

  • 여러 번역 단위에서 동일한 클래스의 Vtable이 생성될 경우
  • 링커가 중복을 제거하고 하나로 통합

3. 런타임 초기화

// 객체 생성 시 Vptr 초기화
Base* obj = new Derived();  // Derived의 Vtable을 가리키도록 Vptr 설정

메모리 구조와 레이아웃

객체 메모리 레이아웃

Vtable 메모리 레이아웃

실제 동작 예제

다형성 구현

void callFunction(Base* obj) {
    obj->func1();  // 런타임에 올바른 함수 호출
}

int main() {
    Base* base = new Base();
    Base* derived = new Derived();
    
    callFunction(base);     // "Base::func1()" 출력
    callFunction(derived);  // "Derived::func1()" 출력
    
    delete base;
    delete derived;
    return 0;
}

Vtable을 통한 함수 호출 과정

  1. obj->func1() 호출
  2. 객체의 Vptr을 통해 해당 객체의 Vtable 접근
  3. Vtable에서 func1()의 인덱스(보통 0번)에 있는 함수 주소 획득
  4. 해당 주소의 함수 실행

생성자에서 순수 가상 함수 호출

https://velog.io/@dev_sensational/C-%EC%88%9C%EC%88%98-%EA%B0%80%EC%83%81-%ED%95%A8%EC%88%98%EC%99%80-Vtable

비가상 소멸자의 치명적 문제

문제 상황: 비가상 소멸자

// 잘못된 예제 - 소멸자가 가상 함수가 아님
class BadBase {
public:
    virtual void func() { cout << "BadBase::func()" << endl; }
    ~BadBase() { cout << "BadBase 소멸자" << endl; }  // 비가상 소멸자!
};

class BadDerived : public BadBase {
private:
    int* data;
    
public:
    BadDerived() {
        data = new int[1000];  // 메모리 할당
        cout << "BadDerived 생성자 - 메모리 할당" << endl;
    }
    
    ~BadDerived() {
        delete[] data;  // 메모리 해제
        cout << "BadDerived 소멸자 - 메모리 해제" << endl;
    }
    
    void func() override { cout << "BadDerived::func()" << endl; }
};

Vtable 구조 비교

올바른 경우 (가상 소멸자)

문제가 되는 경우 (비가상 소멸자)

실제 문제 발생 시나리오

void demonstrateProblem() {
    cout << "=== 비가상 소멸자 문제 시연 ===\n";
    
    BadBase* obj = new BadDerived();  // 파생 클래스 객체 생성
    
    // 가상 함수는 정상 동작 (Vtable 통해 호출)
    obj->func();  // "BadDerived::func()" 출력
    
    // 소멸자 호출 시 문제 발생!
    delete obj;   // BadBase::~BadBase()만 호출됨!
                  // BadDerived::~BadDerived() 호출 안됨!
                  // → 메모리 누수 발생!
}

/* 문제가 되는 출력:
=== 비가상 소멸자 문제 시연 ===
BadDerived 생성자 - 메모리 할당
BadDerived::func()
BadBase 소멸자                    ← BadDerived 소멸자가 호출되지 않음!
                                  → 메모리 누수 및 리소스 누수!
*/

Vtable을 통한 소멸자 호출 과정 분석

가상 소멸자가 있는 경우

class GoodBase {
public:
    virtual void func() { cout << "GoodBase::func()" << endl; }
    virtual ~GoodBase() { cout << "GoodBase 소멸자" << endl; }  // 가상 소멸자
};

class GoodDerived : public GoodBase {
private:
    int* data;
    
public:
    GoodDerived() {
        data = new int[1000];
        cout << "GoodDerived 생성자" << endl;
    }
    
    ~GoodDerived() override {
        delete[] data;
        cout << "GoodDerived 소멸자" << endl;
    }
    
    void func() override { cout << "GoodDerived::func()" << endl; }
};

void demonstrateSolution() {
    cout << "=== 가상 소멸자 해결책 ===\n";
    
    GoodBase* obj = new GoodDerived();
    
    obj->func();  // Vtable을 통한 가상 함수 호출
    
    // delete obj 실행 시:
    // 1. obj의 Vptr을 통해 GoodDerived Vtable 접근
    // 2. 소멸자 엔트리에서 GoodDerived::~GoodDerived() 주소 획득
    // 3. GoodDerived 소멸자 실행 → 메모리 해제
    // 4. 자동으로 부모 클래스 소멸자도 연쇄 호출
    delete obj;   // 올바른 소멸자 체인 실행!
}

/* 올바른 출력:
=== 가상 소멸자 해결책 ===
GoodDerived 생성자
GoodDerived::func()
GoodDerived 소멸자             ← 파생 클래스 소멸자 먼저 호출
GoodBase 소멸자               ← 부모 클래스 소멸자 연쇄 호출
*/

소멸자 호출 순서와 Vtable 상태 변화

// 소멸 과정에서의 Vtable 상태 변화
void destructionProcess() {
    GoodBase* obj = new GoodDerived();
    
    delete obj;  // 소멸 과정 시작
}

소멸 과정의 Vtable 상태 변화:
1. delete obj 호출
[객체] → [GoodDerived Vtable] → GoodDerived::~GoodDerived() 호출

  1. GoodDerived 소멸자 실행 중
    [객체] → [GoodDerived Vtable] (여전히 GoodDerived 타입)
  1. GoodDerived 소멸자 완료, GoodBase 소멸자 시작
    [객체] → [GoodBase Vtable] (Vtable이 부모로 변경됨)
  1. GoodBase 소멸자 실행 중
    [객체] → [GoodBase Vtable]
  1. 완전 소멸 완료

비가상 소멸자로 인한 추가 문제들

1. 메모리 누수

class ResourceHolder : public BadBase {
private:
    std::unique_ptr<int[]> buffer;
    std::fstream file;
    
public:
    ResourceHolder() {
        buffer = std::make_unique<int[]>(1000);
        file.open("temp.txt", std::ios::out);
    }
    
    ~ResourceHolder() {
        // 이 소멸자가 호출되지 않으면:
        // 1. unique_ptr 소멸자 호출 안됨 → 메모리 누수 (이론적으로)
        // 2. fstream 소멸자 호출 안됨 → 파일 핸들 누수
        file.close();
        cout << "ResourceHolder 리소스 정리" << endl;
    }
};

2. RAII 패턴 파괴

// RAII가 제대로 동작하지 않는 예제
void raiiBroken() {
    BadBase* obj = new ResourceHolder();
    
    try {
        // 일부 작업 수행
        throw std::runtime_error("예외 발생");
    }
    catch (...) {
        delete obj;  // ResourceHolder 소멸자가 호출되지 않음!
                     // → RAII 패턴 완전 파괴
    }
}

컴파일러 경고와 해결 방법

1. 컴파일러 경고 활용

// 많은 컴파일러에서 경고 발생
// warning: 'class BadBase' has virtual functions but non-virtual destructor
class BadBase {
public:
    virtual void func() = 0;
    ~BadBase() {}  // 경고 발생!
};

2. 올바른 해결 방법

class CorrectBase {
public:
    virtual void func() = 0;
    virtual ~CorrectBase() = default;  // 가상 소멸자 명시
    
    // 또는 삭제된 소멸자로 다형성 사용 금지
    // ~CorrectBase() = delete;  // 다형성 사용 시 컴파일 에러
};

3. 모던 C++ 접근법

// 스마트 포인터 사용으로 안전성 보장
#include <memory>

void modernApproach() {
    auto obj = std::make_unique<GoodDerived>();
    obj->func();
    // 스코프 종료 시 자동으로 올바른 소멸자 호출
}

실제 Vtable 동작 확인 예제

#include <iostream>
#include <typeinfo>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base 생성자: Vtable = " << typeid(*this).name() << endl;
        virtualFunc();  
    }
    
    virtual void virtualFunc() {
        cout << "Base::virtualFunc() - Vtable이 Base를 가리킴" << endl;
    }
    
    virtual ~Base() {
        cout << "Base 소멸자: Vtable = " << typeid(*this).name() << endl;
        virtualFunc();  
    }
};

class Derived : public Base {
public:
    Derived() : Base() {
        cout << "Derived 생성자: Vtable = " << typeid(*this).name() << endl;
        virtualFunc();  
    }
    
    void virtualFunc() override {
        cout << "Derived::virtualFunc() - Vtable이 Derived를 가리킴" << endl;
    }
    
    ~Derived() {
        cout << "Derived 소멸자: Vtable = " << typeid(*this).name() << endl;
        virtualFunc();  
    }
};

int main() {
    cout << "=== 객체 생성 ===\n";
    Derived d;
    
    cout << "\n=== 정상 호출 ===\n";
    d.virtualFunc();
    
    cout << "\n=== 객체 소멸 ===\n";
    return 0;
}

정리

Virtual Table은 C++의 다형성을 구현하는 핵심 메커니즘으로, 각 클래스마다 컴파일 타임에 생성되어 런타임에 올바른 함수 호출을 보장합니다. 특히 다음 사항들이 중요합니다.

  • 가상 소멸자 필수: 가상 함수가 하나라도 있으면 소멸자도 반드시 가상으로 선언
  • 생성자에서 순수 가상 함수 호출 금지: Vtable 상태 변화로 인한 런타임 에러 발생
  • 소멸 과정의 Vtable 변화: 파생 → 부모 순서로 Vtable이 변경되며 각각의 소멸자 실행
  • 메모리 안전성: 비가상 소멸자는 메모리 누수와 리소스 누수의 주범
  • 현대적 접근: 스마트 포인터와 RAII 패턴으로 Vtable의 안전성 극대화

Vtable의 동작 원리를 정확히 이해하고 올바른 설계 패턴을 적용하면, 안전하고 효율적인 객체지향 코드를 작성할 수 있습니다.

profile
프로젝트 진행 과정을 주로 업로드합니다

0개의 댓글