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. 런타임 초기화
1
2
| // 객체 생성 시 Vptr 초기화
Base* obj = new Derived(); // Derived의 Vtable을 가리키도록 Vptr 설정
|
메모리 구조와 레이아웃
객체 메모리 레이아웃
Vtable 메모리 레이아웃
실제 동작 예제
다형성 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 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을 통한 함수 호출 과정
obj->func1() 호출- 객체의 Vptr을 통해 해당 객체의 Vtable 접근
- Vtable에서 func1()의 인덱스(보통 0번)에 있는 함수 주소 획득
- 해당 주소의 함수 실행
생성자에서 순수 가상 함수 호출
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
비가상 소멸자의 치명적 문제
문제 상황: 비가상 소멸자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 잘못된 예제 - 소멸자가 가상 함수가 아님
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 구조 비교
올바른 경우 (가상 소멸자)
문제가 되는 경우 (비가상 소멸자)
실제 문제 발생 시나리오
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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을 통한 소멸자 호출 과정 분석
가상 소멸자가 있는 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| 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 상태 변화
1
2
3
4
5
6
| // 소멸 과정에서의 Vtable 상태 변화
void destructionProcess() {
GoodBase* obj = new GoodDerived();
delete obj; // 소멸 과정 시작
}
|
소멸 과정의 Vtable 상태 변화:
delete obj 호출 [객체] → [GoodDerived Vtable] → GoodDerived::~GoodDerived() 호출
GoodDerived 소멸자 실행 중 [객체] → [GoodDerived Vtable] (여전히 GoodDerived 타입)
GoodDerived 소멸자 완료, GoodBase 소멸자 시작 [객체] → [GoodBase Vtable] (Vtable이 부모로 변경됨)
GoodBase 소멸자 실행 중 [객체] → [GoodBase Vtable]
완전 소멸 완료
비가상 소멸자로 인한 추가 문제들
1. 메모리 누수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 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 패턴 파괴
1
2
3
4
5
6
7
8
9
10
11
12
13
| // RAII가 제대로 동작하지 않는 예제
void raiiBroken() {
BadBase* obj = new ResourceHolder();
try {
// 일부 작업 수행
throw std::runtime_error("예외 발생");
}
catch (...) {
delete obj; // ResourceHolder 소멸자가 호출되지 않음!
// → RAII 패턴 완전 파괴
}
}
|
컴파일러 경고와 해결 방법
1. 컴파일러 경고 활용
1
2
3
4
5
6
7
| // 많은 컴파일러에서 경고 발생
// warning: 'class BadBase' has virtual functions but non-virtual destructor
class BadBase {
public:
virtual void func() = 0;
~BadBase() {} // 경고 발생!
};
|
2. 올바른 해결 방법
1
2
3
4
5
6
7
8
| class CorrectBase {
public:
virtual void func() = 0;
virtual ~CorrectBase() = default; // 가상 소멸자 명시
// 또는 삭제된 소멸자로 다형성 사용 금지
// ~CorrectBase() = delete; // 다형성 사용 시 컴파일 에러
};
|
3. 모던 C++ 접근법
1
2
3
4
5
6
7
8
| // 스마트 포인터 사용으로 안전성 보장
#include <memory>
void modernApproach() {
auto obj = std::make_unique<GoodDerived>();
obj->func();
// 스코프 종료 시 자동으로 올바른 소멸자 호출
}
|
실제 Vtable 동작 확인 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| #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의 동작 원리를 정확히 이해하고 올바른 설계 패턴을 적용하면, 안전하고 효율적인 객체지향 코드를 작성할 수 있습니다.