I'm FanJae.

[C++] Virtual Function Table 본문

C++/Basic

[C++] Virtual Function Table

FanJae 2024. 8. 22. 13:59

※ 본 포스트는 코드누리 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
Comments