개요
c++에서 virtual은 상속 개념에서 사용된다. java에서는 상속 관계에서 모든 함수가 가상 함수로 사용되지만, c++에서는 virtual을 명시하는 것과 아닌 것에 큰 차이가 생기기 때문에 유의해서 사용해야 한다.
가장 간단한 예시와 함께 알아보기
class Base {
public:
void foo()
{
cout << "Base";
}
};
class Derived : public Base {
public:
void foo()
{
cout << "Derived";
};
};
int _tmain(int argc, TCHAR* argv[])
{
Base* b = new Derived;
b->foo();
delete b;
return 0;
}
main에서 foo를 호출하면 어떤 함수가 호출될까?
다형성을 생각하면 Derived의 foo가 호출되기를 바라지만
b->foo();
00621039 8B 4D FC mov ecx,dword ptr [b]
0062103C E8 BF FF FF FF call Base::foo (0621000h)
00621041 90 nop
실제로는 위와 같이 Base::foo를 명시적으로 호출함을 알 수 있다.
따라서 Base의 foo에 virtual을 붙여서 테스트 해보자.
b->foo();
00771073 8B 4D FC mov ecx,dword ptr [b]
00771076 8B 11 mov edx,dword ptr [ecx]
00771078 8B 4D FC mov ecx,dword ptr [b]
0077107B 8B 02 mov eax,dword ptr [edx]
0077107D FF D0 call eax
0077107F 90 nop
Base의 foo에 virtual을 붙이니 b->foo()의 어셈블리 코드가 위와 같이 바뀌었다.

또한 Derived가 출력되는 것을 볼 수 있다.
어셈블리 코드만 보면 b(객체의 주소)안의 값을 edx에 불러오고 edx안의 값을 eax에 불러와서 이를 call 한다.
call하는 것으로 보아 eax는 함수일텐데 어떻게 된것인지 알아보자.
가상 함수 테이블
위의 동작은 객체에 생성되는 가상 함수 테이블을 이해하면 알 수 있다.
C++에서 virtual 함수가 존재하는 클래스의 생성자 내부에서는 다음과 같은 어셈블리 코드가 실행된다:
CPP_Study.exe!Base::Base(void):
007710E0 55 push ebp
007710E1 8B EC mov ebp,esp
007710E3 51 push ecx
007710E4 89 4D FC mov dword ptr [this],ecx
007710E7 8B 45 FC mov eax,dword ptr [this]
007710EA C7 00 D4 31 77 00 mov dword ptr [eax],offset Base::`vftable' (07731D4h)
007710F0 8B 45 FC mov eax,dword ptr [this]
007710F3 8B E5 mov esp,ebp
007710F5 5D pop ebp
007710F6 C3 ret
특히 007710EA 줄에서는 해당 객체의 시작 주소에 vftable의 주소를 저장하는 것을 볼 수 있다.
이로써 객체 메모리의 가장 앞부분에는 가상 함수 테이블의 포인터가 저장된다는 사실을 알 수 있다.
상속에서의 virtual table
class A
{
virtual void foo1()
{
cout << "A1" << '\n';
}
virtual void foo2()
{
cout << "A2" << '\n';
}
virtual void foo3()
{
cout << "A3" << '\n';
}
void foo4()
{
cout << "A4" << '\n';
}
};
class B : public A
{
virtual void foo1()
{
cout << "B1" << '\n';
}
void foo2()
{
cout << "B2" << '\n';
}
void foo4()
{
cout << "B4" << '\n';
}
};
class C : public B
{
void foo1()
{
cout << "C1" << '\n';
}
void foo4()
{
cout << "C4" << '\n';
}
};
int _tmain(int argc, TCHAR* argv[])
{
A a;
B b;
C c;
return 0;
}
이 경우 가상 함수 테이블은 어떻게 설정될까?
C++에서는 객체가 생성될 때, 생성자는 부모 클래스 → 자식 클래스 순으로 호출되며, 각 생성자 내에서 자신의 vftable을 설정한다.
따라서 실제로 객체가 생성될 때 가상 함수 테이블은 다음과 같이 단계적으로 덮어써지며 설정된다:
- 가장 먼저 A 클래스의 vtable이 설정된다.
- 이후 B 클래스 생성자가 호출되며 B에서 재정의한 가상 함수(예: foo1)가 반영된 새로운 vtable로 덮어쓴다.
- 마지막으로 C 클래스 생성자에서 또다시 vtable이 갱신되어 최종적으로 C의 가상 함수 구현이 반영된다.
이러한 방식으로 C++은 상속 구조 내에서 각 객체가 올바른 가상 함수 동작을 수행할 수 있도록 객체 생성 시점에 vtable을 순차적으로 설정한다.

A, B, C 각각의 virtual table을 조사식을 통해 보면 위와 같다.
가상함수테이블 주소를 타고가면, 해당 위치에 배열처럼 가상함수들을 관리하고 있음을 알 수 있다.

또한 C를 생성하는 코드의 어셈블리를 보면 vftable 주소를 A->B->C 순으로 교체하고 있음을 확인할 수 있다.
소멸자도 확인해보면 소멸하면서 C->B->A 순으로 가상함수테이블을 교체한다.
추가적으로 c++11이후에는 override 키워드를 활용할 수 있다.
overiide 키워드를 붙이지 않는다고 컴파일이 안되는 것은 아니지만, 문법 오류 등을 잡기 좋고, 명시적으로 overriding을 하고 있음을 나타낼 수 있다.
'C, C++ > C, C++ 잡지식' 카테고리의 다른 글
| CreateThread와 _beginthreadex 간단하게 알아보기 (0) | 2025.10.08 |
|---|---|
| 구조체? (0) | 2025.06.14 |
| C++ 캐스팅 연산자 (0) | 2025.06.14 |
| malloc과 new, free와 delete (2) | 2025.05.31 |
| 동적할당 분석해보기(1) (0) | 2025.05.01 |