동적 바인딩 ( Dynamic Binding )이 필요한 이유
함수명과 함수를 이루는 코드가 있는 메모리 주소를 매칭시키는 것을 함수 바인딩이라고 합니다.
그래서 함수를 호출하면, 함수를 이루는 코드가 있는 주소로 이동해서 함수를 수행합니다.
이런 함수 바인딩을 컴파일 시에 수행하면, 정적(static) 바인딩이라고 합니다.
그리고, 일반 멤버 함수는 정적 바인딩을 거칩니다.
그럼, 가상 함수는 정적 바인딩을 할 수 있을까요?
우선, 가상 함수가 어떤 함수인지를 다시 떠올리는 것이 이해에 도움이 될 것입니다.
이곳에 자세한 내용을 링크해 두었습니다.
아래의 예제를 보면 정적 바인딩을 하는데 큰 문제는 없어 보입니다.
#include <iostream>
class CBase{
public:
virtual void VFunc(){
cout << "Base Virtual Func called\n";
}
};
class CDerived : public CBase {
public:
virtual void VFunc(){ // 가상 멤버 함수 재정의
cout << "CDerived Virtual Func called\n";
}
};
int main(){
CDerived derived;
CBase& rObj = derived;
rObj.VFunc(); // 가상 멤버 함수
return 0;
}
컴파일 시 rObj이 참조하는 객체를 분석해서 CDerived 클래스의 함수와 바인딩을 하면, CBase 타입 참조를 통해 CDerived의 함수를 호출할 수 있고, 다형성(polymorphism)을 구현할 수 있을 것입니다.
이전 글을 보셨다면 알겠지만, 가상 함수를 사용하는 목적은 다형성을 구현하기 위한 것임을 잊으면 안 됩니다.
하지만, 다음과 같이 컴파일 시에 객체의 클래스를 분석할 수 없는 경우도 있습니다.
int main(){
CBase base;
CDerived derived;
int num;
cin >> num; // 숫자를 입력받아 다른 결과를 내려고 한다
CBase* pObj;
if ( num % 2 == 0){
pObj = &base;
}
else{
pObj = &derived;
}
cout << "num = " << num << endl;
pObj->VFunc(); // 다형성을 위해 가상 함수 호출
return 0;
}
▼출력
num = 1
CDerived Virtual Func called
num = 2
Base Virtual Func called
위 예제는 사용자가 짝수를 입력하면 CBase의 VFunc 함수를 호출하고, 홀수를 입력하면 CDerived의 VFunc 함수를 호출하는 예제입니다.
이처럼, 사용자가 숫자를 입력할 때까지 함수 바인드를 수행할 수 없는 경우가 있습니다.
따라서, 가상 함수에는 정적 바인딩이 아닌 다른 함수 바인딩 방법이 필요합니다.
C++은 이러한 문제를 해결하기 위해 가상 함수 테이블을 사용합니다.
가상 함수 테이블 ( Virtual Function Table )
가상 함수 테이블은 가상 함수를 선언한 클래스를 위해 컴파일러가 자동으로 만드는 매핑 테이블입니다.
여기에는 가상 함수와 함수 주소가 매칭되어 있습니다.
다음과 같은 가상 함수를 가진 클래스들을 예로 들어보겠습니다.
class CBase{
public:
virtual void VFunc1(){} // 가상 함수1
virtual void VFunc2(){} // 가상 함수2
virtual void VFunc3(){} // 가상 함수3
};
class CDerived : public CBase {
public:
virtual void VFunc2(){} // 가상 함수2 재정의
};
모든 가상 함수들은 가상 함수 테이블에 함수 주소와 함께 기록됩니다.
그래서, 위의 클래스들의 가상 함수 테이블은 아래의 이미지와 같이 구성되어 있습니다.
참고로, 일반 멤버들은 가상 함수 테이블에 기록할 필요가 없습니다.
정적 바인딩을 수행하고, 심지어 정적 바인딩이 더 빠르기 때문이죠.
CDerived 클래스에서 상속 시 CBase 클래스의 가상 함수 테이블을 복사하고, 재정의한 가상 함수만 자신의 함수 주소와 매칭시킵니다.
이렇게 함으로써, 필요한 부분에 한하여 다형성을 구현할 수 있습니다.
이후, CDerived 클래스의 객체의 가상 함수 VFunc2를 호출하려면 먼저 CDerived의 가상 함수 테이블을 찾습니다.
그리고, 테이블에 있는 VFunc2 함수와 함수 주소를 바인딩합니다.
이 함수 바인딩은 가상 함수가 호출될 때 수행됩니다.
이러한 함수 바인딩을 정적(static) 바인딩의 반대 의미로, 동적(dynamic) 바인딩이라고 부릅니다.
또는, 정적 바인딩을 컴파일 시에 수행되는 것이라는 의미로 초기(early) 바인딩이라고 부르고, 동적 바인딩은
후기(later) 바인딩이라고도 부릅니다.
그런데, CBase 타입의 참조 변수로 가상 함수를 호출 시, 어떻게 CDerived 클래스의 가상 함수 테이블을 찾아야 하는지 알 수 있을까요?
CBase 클래스의 가상 함수 테이블을 찾는 것이 먼저 아닐까요?
CDerived derived;
CBase& rObj = derived;
rObj.VFunc2(); // 가상 함수 호출
VPointer
동적 바인딩( Dynamic Binding )을 하기 위해서, 먼저 가상 함수 테이블에 접근해야 합니다.
이때, 가상 함수 테이블의 주소를 담고 있는 VPointer라는 포인터를 사용합니다.
이 VPointer라는 포인터는 눈에 보이지는 않지만 가상 함수를 가진 클래스라면, 컴파일러에 의해 자동으로 주어지는 클래스 멤버입니다.
다음 코드를 통해 VPointer의 존재를 볼 수 있습니다.
#include <iostream>
class CNormalFunc{
void Func(){} // 일반 멤버 함수를 가진 클래스
};
class CVirtualFunc{
virtual void func(){} // 가상 함수를 가진 클래스
};
int main(){
CNormalFunc cNormal;
CVirtualFunc cVirtual;
int nNormal = sizeof(cNormal); // 클래스의 크기 확인
int nVirtual = sizeof(cVirtual);
cout << "normal = " << nNormal << endl;
cout << "virtual = " << nVirtual << endl;
return 0;
}
▼출력
normal = 1
virtual = 8
CVirtualFunc 클래스를 보면, 아무런 멤버가 없음에도 불구하고 객체의 크기가 8이라는 통해, 보이지 않는 멤버가 있는 것을 알 수 있습니다.
이 보이지 않는 멤버가 바로 VPointer입니다.
위에서 얘기한 대로 가상 함수가 있는 경우에만 생성되는 클래스 멤버입니다.
참고로, CNormalFunc 클래스의 크기가 1로 출력되는 이유는, C++에서 크기가 0인 클래스를 생성하지 못하도록 하려고 컴파일러가 더미(dummy) 데이터를 만들기 때문입니다.
다른 멤버를 추가하면, 이 데이터는 없어집니다.
이 VPointer는 클래스의 생성자에서 컴파일러에 의해 가상 함수 테이블의 주소가 입력됩니다.
그래서 가상 함수가 호출되면 먼저 VPointer를 통해 가상 테이블에 접근하고, 호출된 함수와 테이블에 있는 함수 주소를 바인딩하게 되는 것입니다.
정리
- 가상 함수는 동적 바인딩( Dynamic Binding )을 하고, 이를 위해 가상 함수 테이블을 사용한다.
- 컴파일러는 가상 함수 테이블에 접근하기 위해 VPointer를 클래스 멤버로 만든다
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 접근 지정자 public, protected, private 사용법 (0) | 2024.05.29 |
---|---|
[C++] 추상 클래스와 순수 가상 소멸자 (0) | 2024.05.25 |
[C++] 가상 함수( Virtual Function )를 사용하는 이유 (0) | 2024.05.23 |
[C++] 클래스의 멤버 함수 포인터 (member function pointer) (0) | 2024.05.16 |
[C++] 범위 기반 for 루프 (Range-based for loop) 사용법 (0) | 2024.05.14 |