C++ vtable 메커니즘: 가상 함수 동작 원리 완벽 분석
📌 학습 목표
- vtable(Virtual Function Table)의 구조와 동작 원리 이해
- 가상 함수 호출 시 메모리 레벨에서의 동작 과정 분석
- 다형성 구현을 위한 vtable의 역할 완벽 이해
- vtable과 RTTI의 관계 학습
📝 개념 정리
vtable이란?
핵심 개념:
- 가상 함수의 주소들을 저장하는 함수 포인터 테이블
- 클래스마다 하나씩 존재 (객체마다가 아님)
- 컴파일 타임에 생성되어 실행 시 다형성 보장
- vptr(virtual pointer)이 이 테이블을 가리킴
메모리 구조:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void nonVirtualFunc() { cout << "Base::nonVirtual" << endl; }
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
virtual void func3() { cout << "Derived::func3" << endl; }
};
// Base 클래스의 vtable:
// [0] Base::func1의 주소
// [1] Base::func2의 주소
// Derived 클래스의 vtable:
// [0] Derived::func1의 주소 (오버라이드됨)
// [1] Base::func2의 주소 (상속됨)
// [2] Derived::func3의 주소 (새로 추가됨)
💻 동작 흐름 상세 분석
1. 객체 생성과 vptr 초기화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void demonstrateVtableCreation() {
cout << "=== 객체 생성과 vptr 초기화 ===" << endl;
Base* basePtr = new Base();
// 1. Base 객체 메모리 할당
// 2. vptr이 Base 클래스의 vtable을 가리키도록 초기화
Base* derivedPtr = new Derived();
// 1. Derived 객체 메모리 할당
// 2. vptr이 Derived 클래스의 vtable을 가리키도록 초기화
cout << "Base 객체 크기: " << sizeof(Base) << " bytes" << endl;
cout << "Derived 객체 크기: " << sizeof(Derived) << " bytes" << endl;
// vptr 때문에 크기가 증가함 (보통 8바이트 추가, 64비트 시스템)
delete basePtr;
delete derivedPtr;
}
2. 가상 함수 호출 과정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void demonstrateVirtualCall() {
cout << "=== 가상 함수 호출 과정 ===" << endl;
Base* ptr = new Derived();
// ptr->func1() 호출 시 내부 동작:
// 1. ptr이 가리키는 객체의 vptr을 읽음
// 2. vptr이 가리키는 vtable에서 func1의 인덱스(0번) 위치 찾음
// 3. 해당 위치에 저장된 함수 주소를 읽음
// 4. 그 주소로 점프하여 함수 실행
ptr->func1(); // "Derived::func1" 출력
ptr->func2(); // "Base::func2" 출력
delete ptr;
}
3. 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
#include <iostream>
#include <cstdint>
class VtableDemo {
public:
virtual void func1() { cout << "VtableDemo::func1" << endl; }
virtual void func2() { cout << "VtableDemo::func2" << endl; }
void printVtableInfo() {
// vptr은 객체의 첫 번째 멤버
uintptr_t* vptr = *reinterpret_cast<uintptr_t**>(this);
cout << "객체 주소: " << this << endl;
cout << "vptr 주소: " << vptr << endl;
cout << "vtable[0] (func1): " << reinterpret_cast<void*>(vptr[0]) << endl;
cout << "vtable[1] (func2): " << reinterpret_cast<void*>(vptr[1]) << endl;
}
};
class DerivedDemo : public VtableDemo {
public:
void func1() override { cout << "DerivedDemo::func1" << endl; }
void compareVtables() {
VtableDemo base;
DerivedDemo derived;
cout << "=== Base 객체의 vtable 정보 ===" << endl;
base.printVtableInfo();
cout << "\n=== Derived 객체의 vtable 정보 ===" << endl;
derived.printVtableInfo();
}
};
⚡ 주의점과 성능 고려사항
1. 메모리 오버헤드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WithoutVirtual {
int data;
public:
void func() { cout << "Non-virtual" << endl; }
};
class WithVirtual {
int data;
public:
virtual void func() { cout << "Virtual" << endl; }
};
void compareMemoryUsage() {
cout << "WithoutVirtual 크기: " << sizeof(WithoutVirtual) << " bytes" << endl; // 4
cout << "WithVirtual 크기: " << sizeof(WithVirtual) << " bytes" << endl; // 16 (8 for vptr + 4 for data + padding)
}
2. 성능 영향
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
class PerformanceTest {
public:
// 비가상 함수 - 직접 호출, 인라인 최적화 가능
void directCall() { /* 빠른 실행 */ }
// 가상 함수 - 간접 호출, 인라인 최적화 어려움
virtual void virtualCall() { /* 약간의 오버헤드 */ }
};
// 성능 비교 예제
void performanceComparison() {
const int iterations = 10000000;
PerformanceTest obj;
PerformanceTest* ptr = &obj;
auto start = chrono::high_resolution_clock::now();
// 직접 호출
for (int i = 0; i < iterations; ++i) {
obj.directCall();
}
auto mid = chrono::high_resolution_clock::now();
// 가상 함수 호출
for (int i = 0; i < iterations; ++i) {
ptr->virtualCall();
}
auto end = chrono::high_resolution_clock::now();
cout << "직접 호출 시간: "
<< chrono::duration_cast<chrono::microseconds>(mid - start).count()
<< " us" << endl;
cout << "가상 호출 시간: "
<< chrono::duration_cast<chrono::microseconds>(end - mid).count()
<< " us" << endl;
}
🔎 심화 학습: RTTI와 vtable
RTTI (Run-Time Type Information)
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
#include <typeinfo>
class RTTIDemo {
public:
virtual ~RTTIDemo() = default; // RTTI를 위해 가상 함수 필요
};
class RTTIDerived : public RTTIDemo {
public:
void uniqueFunction() { cout << "RTTIDerived specific" << endl; }
};
void demonstrateRTTI() {
RTTIDemo* ptr = new RTTIDerived();
// typeid 연산자 - vtable의 type_info 사용
cout << "실제 타입: " << typeid(*ptr).name() << endl;
// dynamic_cast - vtable 정보 활용
RTTIDerived* derivedPtr = dynamic_cast<RTTIDerived*>(ptr);
if (derivedPtr) {
cout << "dynamic_cast 성공!" << endl;
derivedPtr->uniqueFunction();
}
delete ptr;
}
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
// 실제 vtable은 다음과 같은 구조를 가짐:
// [-2] offset to top (다중 상속에서 사용)
// [-1] type_info* (RTTI를 위한 타입 정보)
// [0] 첫 번째 가상 함수 주소
// [1] 두 번째 가상 함수 주소
// ...
class VtableStructure {
public:
virtual ~VtableStructure() = default;
virtual void func1() = 0;
virtual void func2() = 0;
void analyzeVtableStructure() {
uintptr_t* vptr = *reinterpret_cast<uintptr_t**>(this);
// type_info 포인터 접근 (vptr[-1])
const type_info* ti = reinterpret_cast<const type_info*>(vptr[-1]);
cout << "타입 정보: " << ti->name() << endl;
// 가상 함수 주소들
cout << "가상 소멸자: " << reinterpret_cast<void*>(vptr[0]) << endl;
cout << "func1 주소: " << reinterpret_cast<void*>(vptr[1]) << endl;
cout << "func2 주소: " << reinterpret_cast<void*>(vptr[2]) << endl;
}
};
💡 실무 적용 팁
1. 가상 함수 사용 가이드라인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 좋은 예: 다형성이 필요한 경우
class GraphicObject {
public:
virtual void draw() = 0;
virtual void move(int x, int y) = 0;
virtual ~GraphicObject() = default;
};
// 나쁜 예: 불필요한 가상 함수
class Point {
public:
virtual int getX() const { return x; } // 단순 getter에 virtual 불필요
virtual int getY() const { return y; }
private:
int x, y;
};
2. 가상 소멸자의 중요성
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
class Base {
public:
Base() { data = new int[100]; }
// 가상 소멸자 없으면 메모리 누수 발생
virtual ~Base() {
delete[] data;
cout << "Base destructor" << endl;
}
private:
int* data;
};
class Derived : public Base {
public:
Derived() { moreData = new int[200]; }
~Derived() {
delete[] moreData;
cout << "Derived destructor" << endl;
}
private:
int* moreData;
};
void virtualDestructorDemo() {
Base* ptr = new Derived();
delete ptr; // 가상 소멸자 덕분에 Derived 소멸자도 호출됨
}
3. 성능 최적화 기법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 템플릿을 이용한 정적 다형성 (vtable 오버헤드 없음)
template<typename T>
class StaticPolymorphism {
public:
void execute() {
static_cast<T*>(this)->implementation();
}
};
class FastImplementation : public StaticPolymorphism<FastImplementation> {
public:
void implementation() {
cout << "Fast implementation - no vtable overhead" << endl;
}
};
🌐 외부 링크
다음 학습 주제
- 가상 함수 고급 기법: vptr 최적화, 함수 포인터 vs 가상 함수
- 다중 상속과 vtable: 가상 상속, vtable 레이아웃 복잡성
- 최신 C++ 기능: final, override 키워드 활용
- 성능 최적화: 템플릿 기반 정적 다형성, CRTP 패턴
🪞 회고 질문
- vtable의 메모리 구조와 vptr의 관계를 정확히 설명할 수 있는가?
- 가상 함수 호출 시 발생하는 성능 오버헤드의 정확한 원인을 아는가?
- RTTI가 vtable을 어떻게 활용하는지 이해하고 있는가?
- 언제 가상 함수를 사용하고 언제 피해야 하는지 판단할 수 있는가?
This post is licensed under CC BY 4.0 by the author.