Corgi Dog Bark

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Vtable - 어디에 존재하는가..
    뜯고 또 뜯어보는 컴퓨터/씨쁠쁠 C++ 2022. 11. 3. 00:59
    반응형

    0. Vtable 이란


    vtable 이란, Virtual Function Table의 약자로, 상속 구조에서 메서드를 virtual로 선언하게 되면, vtable이 생성되게 됩니다. vtable(가상 테이블)은 특수한 메모리 영역을 활용하여 알맞은 코드를 호출하게 되는데, 기본적인 정보는 virtual 메서드가 하나 이상 정의된 클래스마다 vtable은 하나씩 구현되어 있습니다. 따라서 vtable을 가진 클래스를 생성한 객체마다 이 vtable에 대한 포인터를 가지게 됩니다. 

     

    vtable은 상속구조에서 동적 바인딩(dynamic binding)이라는 중요한 특성을 가지므로, 꼭! 공부해두시길 바랍니다.

    [그림1. vtable 구조 및 예시]

     

     

     

    1. vTable 실험


    단일 상속구조를 가진, class의 경우 다음과 같이 메모리 구조를 가지게 됩니다.

    #include <iostream>
    
    class box {
    public:
      void size() { std::cout << "box called" << std::endl; }
    
      int boxHeight = 0;
    };
    
    class sqaureBox : public box {
    public:
      void size() {
        std::cout << "sqauareBox called" << std::endl;
      }
    
      int sqaureBoxheight = 0;
    };
    
    int main() {
      box Box;
      sqaureBox squareBox;
    
      std::cout << "Size of Box: " << sizeof(Box) << "\n" // sizeof(4)
                << "Size of SquareBox: " << sizeof(squareBox) << std::endl; // sizeof(8)
    }

    이 코드를 실행시켜보면,

    다음과 같이 부모 클래스는 4바이트(int) 그리고, 상속받은 SqareBox는 8바이트를 띄게 됩니다.

    그리고, vtable을 선언하게 된다면, 다음과 같은 메모리 구조를 띄게 됩니다.

    #include <iostream>
    
    #pragma pack(1)
    class box {
    public:
      virtual void size() { std::cout << "box called" << std::endl; }
    
      int boxHeight = 0;
    };
    
    class sqaureBox : public box {
    public:
      virtual void size() override {
        std::cout << "sqauareBox called" << std::endl;
      }
    
      int sqaureBoxheight = 0;
    };
    
    int main() {
      box Box;
      sqaureBox Square;
    
      std::cout << "Size of Box: " << sizeof(Box) << "\n"
                << "Size of Square: " << sizeof(Square) << std::endl;
    }

    1 바이트 정렬을 위해, #pragma pack을 사용해주었으며, vtable을 사용하게 되어, 추가적으로 vtable을 가리키는 포인터인 8바이트만큼 메모리 양이 증가한 것을 확인할 수 있습니다.

    마찬가지로, 다중 상속 시 다음과 같이 메모리 배열이 바뀌는 것을 확인할 수 있습니다.

    #include <iostream>
    
    #pragma pack(1)
    class box {
    public:
      virtual void size() { std::cout << "box called" << std::endl; }
    
      int boxHeight = 0;
    };
    
    class sqaureBox : public box {
    public:
      virtual void size() override { std::cout << "sqauareBox called" << std::endl; }
    
      int sqaureBoxheight = 0;
    };
    
    class rectangleBox : public sqaureBox {
    public:
      virtual void size() override { std::cout << "rectangleBox called" << std::endl; }
    
      int rectangleBoxheight = 0;
    };
    
    int main() {
      box Box;
      sqaureBox Square;
      rectangleBox Rectangle;
      std::cout << "Size of Box: " << sizeof(Box) << "\n"
                << "Size of Square: " << sizeof(Square) << "\n"
                << "Size of Rectangle: " << sizeof(Rectangle) << std::endl;
    }

    - vtable을 가리키는 포인터의 크기는 유지되어, 멤버 데이터인 int(4) 만큼만이 증가하게 됩니다.

     

    반응형

    다중 상속 시, vTable 그리고 vTable pointer


    vtable을 가르키는 포인터는 객체 생성 시 맨 앞에 위치하게 되는데, 따라서 class를 생성하고, non-pod 객체에 대해서 memset( )이나 기타 메모리를 통째로 초기화시켜주면 안 되는 이유가 여기서 발생하게 됩니다. vTable은 각 컴파일러마다 효육적으로 구현하게 되고, 이를 vTable Pointer를 통해 접근하게 됩니다.

     

    이때, 각 객체마다 vTable을 가리키는 포인터가 객체 앞에 생기게 된다는 점이 중요하고, 각 virtual 멤버 함수마다 생기는 것이 아니라는 점 또한 유의해야 합니다. 그리고 다중 상속 시, 각 vtable을 가리키는 포인터가 추가되므로 그만큼 메모리는 증가하게 됩니다. 아래 예시를 보게 되면 다중 상속 시, 28로 크기가 늘어난 것을 확인할 수 있습니다. 

    - vTable1(8) + vTable(8) + BoxInt(4) + xBoxInt(4) + Rectangle(4) = 28

     

    #include <iostream>
    
    #pragma pack(1)
    class box {
    public:
      virtual void size() { std::cout << "box called" << std::endl; }
    
      int boxHeight = 0;
    };
    
    class xBox{
    public:
      virtual void xSize() { std::cout << "xBox called" << std::endl; }
    
      int sqaureBoxheight = 0;
    };
    
    class rectangleBox : public box, xBox {
    public:
      virtual void size() { std::cout << "rectangleBox called" << std::endl; }
    
      int rectangleBoxheight = 0;
    };
    
    int main() {
      box Box;
      xBox Xbox;
      rectangleBox Rectangle;
      std::cout << "Size of Box: " << sizeof(Box) << "\n"
                << "Size of Xbox: " << sizeof(Xbox) << "\n"
                << "Size of Rectangle: " << sizeof(Rectangle) << std::endl;
    }

     

     

    Vtable은 그래서 어딨지?


    이에 대한 답이 msdn forum에서 찾아왔습니다.

    https://social.msdn.microsoft.com/Forums/en-US/0e2d1b36-1805-46de-afe8-ce7b6348fe28/where-the-vtable-memory-will-be-sotred?forum=vclanguage

     

    where the vtable memory will be sotred?

    When you make any function virtual, the compiler will insert a vptr inside your class. As a result, the size of the class will grow by 4 bytes (on Win32).This pointer holds the address of the virtual table (vtable). vtable is constructed by the compiler at

    social.msdn.microsoft.com

    When you make any function virtual, the compiler will insert a vptr inside your class. As a result, the size of the class will grow by 4 bytes (on Win32). This pointer holds the address of the virtual table (vtable). vtable is constructed by the compiler at compile time and is basically nothing but an array of function pointers. The function pointers are actually pointers to the virtual functions of that particular class. To be more exact, the virtual table is a 
    static array of function pointers
    , so that different instances of the same class can share that vtable. Since, static members are stored in the data section (. data), the vtable is also stored in the data section of the executable.

    간단하게 말하자면, vPointer가 vTable을 가리키게 되고, vTable은 컴파일러에 의해서 함수 포인터를 가리키는 자료구조로 완성이 됩니다. 또한 가르키는 함수 포인터는 특정 객체에 맞는 포인터를 가지게 됩니다. 그리고 마지막 문장에서 vTable의 위치를 나타내는 말을 했는데, data 영역에 저장된다고 합니다. 그 이유는 생성된 객체가 vTable을 통해 접근해야 되는데, 이는 전역으로 공유되어야 하기 때문이라고 합니다.

     

     

    긴 글 읽어주셔서 감사합니다.

    도움이 되셨거나, 잘못된 정보가 있으면 댓글 부탁드립니다. 

    반응형

    댓글

Designed by Tistory.