[C/C++] Virtual Table

kjh3865·2020년 9월 19일
0

C++

목록 보기
6/23
post-thumbnail

가상함수 테이블(Virtual Table) 소개

class Animal {
	virtual void func();
    	int var;
}

class Dog : public Animal {
	void func() override;
    	int color;
}

클래스에 가상함수를 선언하면 코드 영역에 가상함수 테이블이 생성되는데 가상함수 테이블을 만들면 객체의 메모리 영역에 가상 테이블을 가리킬 포인터(*)가 생성된다.
실제로 해당 클래스의 크기는 4byte가 아닌 32비트 포인터가 추가되어 8byte가 된다. (32bit os)
이 포인터는 가상함수 테이블을 가리키게 된다.

  • Animal 클래스의 힙 메모리 구조

보통 VTalbe을 가리키는 포인터는 virtual 키워드가 선언된 부모 클래스에 생성된다.
자식 클래스가 final 클래스일 경우 자식 클래스에는 VTable을 가리키는 포인터가 생성되지 않는다.


Virtual Table의 기능

가상함수 테이블은 코드영역(Code)에 선언되며 테이블안에는 virtual 함수를 가리키는 주소가 들어있다.
즉 동적할당 후 오버라이딩 된 함수를 호출하는 방식은 객체 내부의 포인터가 가리키는 가상 테이블을 보고 알맞는 오버라이딩 함수를 호출하는 방식이다. (객체의 상태에 따라 베이스 함수가 될 수 있고 상속된 자식 함수가 호출될 수도 있다)

Animal * dog = new Dog();
  • 위 객체의 힙 영역은 다음과 같은 구조를 지닌다.

(Dog 클래스에는 가상 함수 오버라이드 된 가상 함수 func()가 존재한다)

자식 멤버인 color가 dog의 힙에 존재하는 이유는 new로 객체를 할당 받을 때 포인터가 가리키는 heap의 크기는 할당된 (자식)클래스의 크기가 된다. 단지 pointer의 자료형이 부모 클래스 Animal로 만들어졌기 때문에 언어의 구문에 따라 Animal 클래스에 정의된 attribute만 접근할 수 있는 거다.

이렇게 함수에 virtual 키워드를 사용하면 베이스 클래스와 상속하는 파생 클래스 모두
Virtual Table을 Code 영역에 생성하고 동적할당 시 어떤 클래스를 할당하느냐에 따라
Heap의 포인터가 어떤 가상 테이블을 가리킬 지 결정한다. 그래서 오버라이드 된 함수가
호출되는 방식은 포인터가 가리키는 가상 테이블을 통해 해당 함수를 호출하는 방식이다.


객체의 상속별 메모리(크기) 상태 요약

class Animal { ~virtual func(); double Data; }

일반적인 클래스는 자기 자신만큼의 영역을 가리키는 위와 같은 구조를 지닌다.
하지만 클래스가 상속되었을 경우 바로 밑에 자식의 멤버 변수가 들어오는 형태로 클래스가 재구성된다.
이렇게 구성할 경우 포인터가 자식과 부모 사이를 오가기 매우 쉽기 때문이다.

class Lion : public Animal {}

객체를 동적 할당할 때 어떤 클래스로 동적할당 했느냐에 따라 어느 범위만큼 선정하여 객체를 생성할지 사이즈를 구할 수 있다.

예를 들어, 동적 할당을 자기 자신(Base) 클래스로 할당할 때 객체 구조는 아래의 주황색 영역만큼 생성된다.

Animal * anim = new Animal();

클래스의 총 영역 자체는 파생 클래스를 포함해서 멤버 변수 lion Data를 포함하고 동적 할당을 만약 파생 클래스 Lion 클래스로 할당했을 경우 객체의 크기는 lion Data까지 넓혀지게 된다. (객체의 VTable 역시 자신 Animal 클래스의 VTable을 가리키게 된다.)

Animal * lion = new Lion(); // 파생 클래스로 동적 할당

Lion 클래스로 동적 할당을 했을경우 lion Data까지 객체의 크기가 넓어지고 객체의 VTable은 Lion의 VTable을 가리키게 된다. (VTable에는 해당 클래스의 가상 함수 주소들이 들어있다.)

하지만 분홍색 영역인 포인터 자체는 Animal 자료형인 Animal 클래스 영역만 볼 수 있다. (문법에 의해)

즉, 포인터는 자기 자료형에 맞는 함수를 호출하지만 실제로는 VTalbe을 통해 VTable이 가리키는 클래스의 함수를 호출하는 방식이다.


다중 상속의 경우 객체 구성

번외로 다중 상속의 경우를 생각해볼 수 있는데 Liger 클래스는 Tiger와 Lion 클래스를 다중 상속 받은 클래스다. 따라서 ligerPtr 객체의 힙 구성은 Liger 멤버 변수 ligerData 위에 Lion과 Tiger의 영역이 올려져있는 형태로 구현되있다. 그리고 부모 클래스들은 가상 함수로 인해 각각 VTable을 지니며 VTable은 모두 Liger VTable을 가리키고 있는 상태다. (Liger로 동적 할당을 받았기에 부모의 VTable은 Lion의 VTable을 가리키게 된다.)

profile
C++와 게임 엔진을 주로 다루는 블로그

0개의 댓글