가상 함수 테이블 (Virtual function table)
가상 함수 테이블은 가상 함수에 대해 연결되어 있는 함수 포인터를 저장하는 공간입니다. 기본적인 원리는 다음과 같습니다.
- 클래스 내에 가상 함수가 존재하는지 확인합니다.
- 클래스 내에 가상 함수가 있다면, 가상 함수 테이블을 생성합니다. 그리고 이 테이블의 포인터를 인스턴스의 가장 앞 혹은 뒤에 항상 추가합니다.
- 가상 함수를 호출할 때, 위의 테이블을 참조하여 실제 호출될 함수를 찾습니다.
아래의 예제는 vtable로 부터 함수를 찾아 호출하는 예제입니다. 시스템에 따라 동작하지 않거나 크래시가 발생할 수 있습니다.
#include <iostream>
class Person
{
public:
void DoAction()
{
Action();
void (*actionPtr)() = (void (*)())(**(void***)this); // from vtable.
actionPtr();
}
private:
virtual void Action() = 0;
};
class Student : public Person
{
private:
virtual void Action()
{
std::cout << "Study" << std::endl;
}
};
class Employee : public Person
{
private:
virtual void Action()
{
std::cout << "Work" << std::endl;
}
};
int main(int argc, char** argv)
{
Student student;
Employee employee;
student.DoAction();
std::cout << std::endl;
employee.DoAction();
}
Study
Study
Work
Work
- 우선 Instance의 가장 처음 혹은 끝에 '가상 함수 테이블'에 대한 포인터가 존재하므로
this 포인터를 void***으로 캐스팅 합니다. (다른 멤버 변수가 없으므로)
-
void***(가상 함수 테이블의 주소를 담는 포인터 == this)에 포인터 연산 1회로 '가상 함수 테이블'의 주소를 얻습니다.
-
void** (실제 연결된 함수의 주소를 담는 포인터 == 가상 함수 테이블)에서 포인터 연산 1회로 '실제 연결되어 있는 함수'의 주소를 얻습니다.
가상 함수 테이블은 실제 연결되어 있는 여러 함수 들을 Array처럼 보관합니다.
-
void* (실제 연결되어 있는 함수의 주소를 담는 포인터)을 void (*)() (함수 포인터)로 캐스팅합니다.
-
함수 포인터를 호출하면, 알맞는 가상 함수가 호출됩니다.
위에서 확인 가능한 것 처럼, 가상 함수를 사용하면 '컴파일러가 임의의 데이터를 덧 붙이는 과정'이 있습니다. C style의 메모리 동적할당 (malloc)을 이용해서 메모리를 할당 하고, 이를 클래스로 캐스팅 해서 사용하는 경우 가상 함수 테이블에 대한 연결이 없기 때문에 문제를 일으킬 수 있습니다.