일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- std::cout
- increment operator
- dynamic_cast
- std::ostream
- return by reference
- c++ basic practice
- discord bot
- delete function
- conversion constructor
- virtual function
- virtual inheritance
- vector capacity
- this call
- member function pointer
- constructor
- C++
- vector size
- virtual destructor
- pointer to member data
- virtual function table
- operator overloading
- suffix return type
- c++ multi chatting room
- base from member
- placement new
- std::vector
- diamond inheritance
- new&delete
- 더 지니어스 양면포커
- std::endl
- Today
- Total
I'm FanJae.
[C++] Virtual Function Table 본문
※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.
1. 가상 함수에 대한 의문점
class Base
{
int bm{0};
public:
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
};
class Derived : public Base
{
int dm{0};
public:
void f2() override {}
};
int main()
{
Base base;
Dervied derived;
Base *p = &derived;
p->f2();
}
- 지난 시간 가상 함수에 대해 다뤘다.
- 이때, p->f2(); 에서 어느 함수를 호출 할 것인가에 대한 이야기이다.
- 지난 시간에 정리했지만, 이는 다음과 같다.
※ 가상 함수가 아닌 경우
- 컴파일 시간에 p의 타입으로 호출을 결정한다.
- p는 Base* 이므로 Base::f2를 호출한다
※ 가상함수인 경우
- 실행시간에 p가 가리키는 메모리를 조사해서 호출을 결정한다.
여기서, 2가지의 의문점이 발생한다.
① p가 가리키는 객체가 어떤 타입인지 어떻게 조사를 하는 것일까?
② p가 가리키는 타입을 알아도 어떻게 Derived:f2를 호출 할 수 있는가? 이를 결정하려면 실행시간에 함수 주소를 알고 있어야 가능하다.
2. 가상 함수 호출의 원리
- 이 내용은 C++ 표준에 있는 공식적 내용은 아님. 컴파일러 마다 다르거나 버전에 따라 다를 수 있음.
- 대부분이 이러한 원리를 따라가고 있음.
class Base
{
int bm{0};
public:
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
};
class Derived : public Base
{
int dm{0};
public:
void f2() override {}
};
int main()
{
Base base;
Derived derived;
Base *p = &derived;
p->f2();
}
- Base 클래스에 가상함수 3개가 있다.
- 실행 시간에 이들을 호출 하게 만들기 위해서는 f1,f2,f3의 주소가 메모리 어딘가에 저장되어야 한다.
- 이와 같이 Base의 모든 가상함수의 주소값을 가진 테이블을 만들어낸다.
- Derived Class에 대해서는 f2를 override 했기 때문에, 이와 같이 재정의했기 때문에 재정의한 f2가 들어간다.
- Base의 객체를 만들면 위와 같이 컴파일러가 임의의 포인터를 추가한다.
- 이는 Base 타입의 객체가 자신의 가상함수가 몇번지에 있는지 알기 위해 추가되는 것이다.
- 메모리 레이아웃을 보면, b1,b2, d1은 각각 이런 형태를 띄고 있음을 확인할 수 있다.
- Base *p = &derived; 에 대해서는 이와같이 메모리 레이아웃이 그려진다.
- p->f2(); 가 호출되면 이는 가상함수가 선언된 순서를 가지고 검색한다.
- f2는 2번째 선언된 가상함수 이므로, 가상함수 테이블에서 2번째 항목의 주소를 꺼내어 호출한다.
- 즉, p->f2();에서 포인터가 가리키는 부분을 ptrtable이라고 가정할때, p->ptrtable[2]() 처럼 기계어 코드로 변경한다
2-1. 오버헤드 문제
- 가상함수 테이블은 클래스당 한개의 가상함수 테이블이 생성될 것이다.
- 만약, 가상함수가 많으면 이는 상당히 무시하지 못할 개수이다. (가상함수가 1000개라면, 포인터 1000개의 배열이다.)
- 객체당 한개의 가상함수 테이블을 가리키는 포인터 멤버가 추가된다.
- 가상 함수 호출 시 주소를 꺼내서 호출하므로 이에 대한 약간의 지연이 발생한다.
※ 인라인 치환은 컴파일 타임에 이루어지므로, 인라인 치환도 불가능하다.
2-2. 어셈블리 레벨에서 확인
- Main을 보면 Base에 대한 객체를 하나 만들었고, 이에 대해서 다음과 같이 vtable이 생성된 것을 확인할 수 있다.
- 아직, Derived에 대한 vtable이 없는 것은, 객체가 하나도 생성되지 않았기 때문이다.
- 이를 생성해보면 다음과 같은 것을 확인할 수 있다.
- 다음과 같이 Derived vTable은 자신이 Override한 Derived f2에 대해서만 재정의 vtable에 담기는 것을 확인할 수 있다.
3. Virtual Function과 Default Parameter
#include <iostream>
class Base
{
public:
virtual ~Base() {}
virtual void f(int a = 10)
{
std::cout << "Base : {" << a << "}";
};
};
class Derived : public Base
{
public:
void f(int a = 20) override
{
std::cout << "Derived : {" << a << "}";
};
};
int main()
{
Derived d;
Base* p = &d;
p->f();
}
- 기대한 결과는 Derived : {20}을 기대했겠지만 실제로는 그렇지가 않다.
- 아래 결과를 보면 Derived : {10}이 나온다.
① 원인과 결론
- Default Parameter는 컴파일러가 컴파일 시간에 디폴트 값을 채운다.
- 이에 반면, Virtual Function은 어느 함수를 호출할지를 실행 시간에 결정한다.
- 결론은 Virtual Function을 Override 할때는 절대 Default Parameter를 변경하면 안된다.
'C++ > Basic' 카테고리의 다른 글
[C++] Multiple Inheritance, Diamond Inheritance, Virtual Inheritance (0) | 2024.08.23 |
---|---|
[C++] RTTI, Dynamic Cast (0) | 2024.08.22 |
[C++] Abstract class, Interface (0) | 2024.08.21 |
[C++] Upcasting, Virtual Function (0) | 2024.08.21 |
[C++] Inheritance(상속) (0) | 2024.08.20 |