이번글은 C++의 다형성의 가상함수에 대한 글입니다.
C++ 다형성에 대해서 전부는 다룰 수 없고 제가 이해한 내용중에서도
"정적 바인딩", "동적 바인딩", "가상 함수", "vftable", "순수 가상함수", "추상 클래스" 정도에 대해서만 다룰려고 합니다.
먼저 아래의 코드에서
class A
{
public:
void Func()
{
cout << "A Func" << endl;
}
};
class AA : public A
{
public:
void Func()
{
cout << "AA Func" << endl;
}
};
void CallFunc(A* a)
{
a->Func();
}
int main()
{
A a;
AA aa;
CallFunc(&aa);
return 0;
}
// 출력 결과
// A func
CallFunc() 함수를 호출 할 경우 A 클래스의 Func가 호출 될까요? 아니면 AA의 Func가 호출 될까요?
당연히 A클래스의 Func가 호출됩니다!
그 이유는 너무나 당연하게도 CallFunc의 매개변수를 A*로 받고 있기 때문입니다!
조금더 자세한 설명을 하자면, C++ 컴파일러는 포인터 연산의 가능성 여부를 판단 할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가르키는 객체의 자료형을 기준으로 판단하지 않습니다!
그래서 분명 AA의 객체를 넘겨주었지만 A클래스의 Func가 호출 된것인데요,
CallFunc입장에서는 그냥 A클래스의 Func를 호출하라고 정적으로 바인딩( a->Func(); ) 되어 있기 때문이기도 합니다!
저는 그냥 A* 와 같은 자료형만 가지로 CallFunc에 AA객체를 넘겨준 경우 AA의 Func를 호출 하고싶고 A의 객체를 넘겨준 경우에는 A클래스의 Func를 호출하고 싶은데 말이죠.
그러면 CallFunc2(AA* a) 를 하나 더 만들어야 할까요? 이러면 C++의 다형성을 사용하는 이유가 없어지죠. 유지보수도 힘들구요.
그러면 CallFunc의 인자에 AA객체를 넘겨준 경우에는 AA의 Func가 호출되도록 하고, CallFunc에 A객체를 넘겨준 경우에는 A클레스의 Func가 호출 되도록 할려면 "동적 바인딩"을 사용하시면 됩니다!
동적 바인딩을 사용할려면 "가상 함수"를 사용해야합니다.
이 가상 함수는 아래와 같이 "vritual" 키워드만 함수 반환형 앞? 왼쪽에 붙여 주시면 됩니다!
class A
{
public:
virtual void Func()
{
cout << "A Func" << endl;
}
};
이렇게 되면 A클래스의 Func함수는 "가상 함수"가 되며, 상속받는 쪽에서의 Func함수도 "가상 함수"가 됩니다.
class AA : public A
{
public:
void Func()
{
cout << "AA Func" << endl;
}
};
현재 A를 상속받는 AA의 멤버함수인 Func도 가상함수인데요, 이렇게만 보면 명확하지 않기 때문에
class AA : public A
{
public:
virtual void Func() override
{
cout << "AA Func" << endl;
}
};
가상 함수라는 것을 명시하기 위한 "virtual" 키워드와 "override" 했다는 것을 명시하기 위한 키워드도 적어주시는게 좋습니다!
(overload와 햇갈리면 안됩니다.)
이렇게 "가상 함수"를 만들어 주게 되면 A클래스를 상속을 받는 모든 클래스의(Func를 override 한경우에) 객체에 맞는 Func를 호출 할 수 있게 됩니다!
그러면, CallFunc는 어떻게 A* a 만 가지고 AA의 Func를 호출 할지 A의 Func를 호출 할지 구분할까요?
이는 "vftable(virtual function table)" 에 의해 가능한 부분입니다.
vftable은 main함수가 호출되기 이전에 메모리 공간에 할당되며, A라는 객체를 생성함과 동시에 A의 생성자의 초기화 부분에서 가상함수 테이블 주소를 메모리 앞쪽에 할당받게 됩니다.
실제로 그런지 확인을 해보도록 하겠습니다.
A의 인스턴슬를 생성하는 부분에 breakpoint를 걸어 디스어셈블리를 보면
a의 주소를 타고 가면 cccc로 쓰레기 값으로 채워져 있는데
디스어셈블리에서 07FF71... 어쩌구 하는 부분을 call하는 부분을 볼 수 있습니다. 그리고 F11로 쭉쭉 타고 가다보면
rcx 레지스터에 vftable 이라는 것을 넣는 것을 볼 수 있습니다.
그러면 A의 주소의 앞부분에 12deac20이라는 녀석이 끼워진 것을 볼 수 있는데요.
아까 보았던 07FF71..어쩌구가 'vftable' 옆에 있네요.. 이게 가상함수 테이블 주소라고 추측해볼 수 있겠습니다.
AA의 경우에도 A와 거의 동일한 방식으로 가상함수 테이블이 채워지게 됩니다.
디테일 이긴 하지만 AA의 경우 A의 가상함수를 먼저 할당받은 다음 AA의 가상함수 테이블 주소를 덮어 씁니다.
따라서, CallFunc에서는 A*를 가지고도 a의 가상함수 테이블을 참조하여
AA객체를 넘겨준 경우에는 AA의 Func가 호출되도록 하고, CallFunc에 A객체를 넘겨준 경우에는 A클레스의 Func가 호출 되도록 할 수 있었던 것입니다!
감사합니다 :)
'CPP' 카테고리의 다른 글
[C++] 전달참조 (0) | 2023.07.31 |
---|---|
[C++] 오른값 참조 (0) | 2023.07.30 |
[C++] Effective C++ 항목 1~27 정리 (0) | 2023.07.03 |
[C++] C++ 컴파일 의존성 (2) | 2023.05.28 |
[C++] 복사 생성자와 임시객체 (0) | 2023.04.16 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!