개념과 활용/C++

[다형성] 추상 클래스와 가상 함수 테이블

14J 2024. 6. 8. 18:33

광고가 없는 개인 저장용 블로그입니다.

 

가상 함수와 가상 함수 테이블을 알고 있다고 가정한 메모입니다.

자세한 내용을 알고 싶으면 출처를 참조하시길 바라겠습니다.

 

 

1. 가상 함수 테이블

https://cosyp.tistory.com/228 [출처]

 

출처를 타고 가시면 자세한 내용을 읽을 수 있습니다. 해당 사진을 사용한 이유는 링크된 본문의 글을 읽고

가상 함수 테이블을 가지는 여러 상황에 대해 활용하다가 실수할 수 있는, 헷갈리던 개념을 기록하기 위해서입니다. 

 

 

1-1 추상 클래스

 

순수 가상 함수를 하나 이상 가진 클래스일 경우 객체를 생성할 수 없는 추상 클래스로

상속을 받은 클래스에서 반드시 override을 통해 사용하여야 합니다.

 

  • 어떻게 활용하는가?

 

추상 클래스는 객체를 만들지 못할 뿐이지, 추상 클래스 포인터가 함수를 호출할 경우 런타임 다형성을 통해 

가상 함수 테이블을 활용하여 실제 객체가 가지는 가상 함수를 호출합니다. 

이를 활용할 시 추상 클래스의 포인터만 가지고 각 객체에 맞는 함수를 호출하는 다형성을 실현할 수 있습니다.

 


  • 다형성은 객체 지향 프로그래밍(OOP)의 중요한 개념 중 하나로, 
    다양한 데이터 타입에 대해 동일한 인터페이스를 통해 작업을 수행할 수 있도록 하는 특성
  • 런타임 다형성은 주로 상속(Inheritance)과 가상 함수(Virtual Functions)를 통해 구현됩니다. 
    이는 "동적 바인딩" 또는 "늦은 바인딩"이라고도 불리며, 프로그램 실행 시점에 호출할 함수가 결정됩니다.

#include <iostream>
#include <list>



class instrument				// 악기 클래스(추상 클래스)
{								
public:
	virtual void sound() = 0;		// 순수 가상 함수
	// virtual ~instrument() {}
};

class trumpet : public instrument	// 트럼펫 클래스
{
public:
	void sound() override
	{
		std::cout << "Trumpet Sound \n";
	}
};

class piano : public instrument		// 피아노 클래스
{
public:
	void sound() override
	{
		std::cout << "Piano Sound \n";
	}
};


int main()
{

	std::list<instrument*> li;

	trumpet test1;
	piano test2;
	

	li.push_back(&test1);			// 업 캐스팅의 경우 자동으로 수행
	li.push_back(&test2);			// 업 캐스팅의 경우 자동으로 수행	
	
	for (auto it : li)
	{
		it->sound();
	}
	
	return 0;
}

 

출력 결과

 

해당 코드를 살펴보면 순수 가상 함수 클래스인 instrument(악기) 클래스를 상속 받은

트럼펫 클래스와 피아노 클래스 객체의 주소를

추상 클래스의 포인터인 <instrument*> 타입을 받아 저장하는 리스트에 추가하고 있습니다.

 

이 과정에서 암시적 업 캐스팅이 발생하여 instrument 포인터로 변환되어 추가됩니다.

이는 상속 관계에 있는 클래스에 한 해 자동으로 수행됩니다.

 

이후 리스트를 순회하며 각 객체의 sound() 함수를 호출합니다.

다형성 또는 동적 바인딩을 통해 올바른 파생 클래스의 sound() 함수 호출을 유도할 수 있습니다.

이 때 객체는 가상 함수 테이블을 참조하여 자신이 수행할 함수를 호출합니다.

 

1-2 업 캐스팅 / 다운 캐스팅

 

위의 코드를 살펴보면 리스트에 객체의 주소가 추가될 때, 베이스 클래스인 instrument 클래스의 타입에 맞게 

암시적 업 캐스팅이 되어 리스트에 추가됩니다.

 

이를 통해 베이스 포인터를 통해 함수를 호출 하더라도 실제 객체가 가지는 가상 함수 테이블을 참조하여

객체에 맞는 sound() 함수의 호출을 유도할 수 있습니다. 

 

그렇다면 다운 캐스팅은 어떠한 상황에서 사용될 수 있을까요?

class instrument					// 악기 클래스
{
public:
	virtual void sound() = 0;
	// virtual ~instrument() {}
};

class trumpet : public instrument	// 트럼펫 클래스
{
public:
	explicit trumpet(int n) : m_Trumpet_State(n) {}
	void sound() override
	{
		std::cout << "Trumpet Sound \n";
	}

	int stateGetter()
	{
		return m_Trumpet_State;
	}
private:
	int m_Trumpet_State;
};



class piano : public instrument		// 피아노 클래스
{
public:
	explicit piano() {}

	void sound() override
	{
		std::cout << "Piano Sound \n";
	}


};

 

추가된 내용은 상속받은 클래스가 고유한 멤버 변수를 가지고 있다고 가정 할 때, 

해당 객체의 멤버 변수를 참조하여 sound()를 재생하고 싶다고 가정한다면.

( 트럼펫 객체의 경우만 멤버 변수 m_Trumpet_State를 참조하여 3이 넘은 객체만 sound() 함수를 호출 하고 싶다면 )

 

int main()
{

	std::list<instrument*> li;

	trumpet test1(1);
	piano test2(2);
	

	li.push_back(static_cast<instrument*>(&test1));			// 업 캐스팅의 경우 자동으로 수행
	li.push_back(static_cast<instrument*>(&test2));			// 업 캐스팅의 경우 자동으로 수행	

	for (auto it : li)
	{
		if (trumpet* temp = dynamic_cast<trumpet*>(it))		// temp = nullptr을 반환 받지 않았다면
		{
			if (temp->stateGetter() >= 3)					// m_Trumpet_State의 값이 3이 넘을 때만
			{
				it->sound();
			}
		}
		else						 // dynamic_cast를 통해 형 변환시 실제 객체가 
        							// trumpet 객체가 아니였을 경우
		{
			it->sound();		    // 항상 sound() 함수를 호출	
		}
	}
	
}

 

이런 식으로 사용할 수 있습니다. 

dymamic_cast 의 경우 상속 관계에 있는 포인터를 업 캐스팅 또는 다운 캐스팅을 진행하여

실제 객체가 변환하려는 포인터와 맞지 않은 객체라면 nullptr을 반환합니다.

 

  • dynamic_cast<>는 상속 관계에 있는 포인터여야 하며 , virtual 키워드를 통해 가상 함수를 가져야만 타입을 참조할 수 있다.

vtable:

+----------------------------------+

| RTTI 정보에 대한 포인터 |   <-- 타입 정보 참조   // 이 주소를 타고 이동하여 객체의 타입을 참조할 수 있음

+----------------------------------+

| 가상 함수 #1 주소 |              <-- 가상 함수 1

+----------------------------------+

| 가상 함수 #2 주소 |              <-- 가상 함수 2

+----------------------------------+

| 가상 함수 #3 주소 |              <-- 가상 함수 3

+----------------------------------+


 

 

2 가상 함수 테이블의 상속

 

가상 함수가 하나 라도 있을 경우 만들어지는 가상 함수 테이블의 경우

이를 상속받은 클래스가 별도의 override를 진행하지 않은 경우, 같은 가상 함수의 주소를 상속받습니다.

 

 

극단적으로 piano 클래스가 trumpet 클래스를 상속 받는다고 가정 한다면

class instrument					// 악기 클래스
{
public:
	virtual void sound() = 0;
	// virtual ~instrument() {}
};

class trumpet : public instrument	// 트럼펫 클래스
{
public:
	explicit trumpet(int n = 0) : m_Trumpet_State(n) {}
	void sound() override
	{
		std::cout << "Trumpet Sound \n";
	}

	int stateGetter()
	{
		return m_Trumpet_State;
	}
    virtual ~trumpet() {}
private:
	int m_Trumpet_State;
};



class piano : public trumpet		// 아무튼 상속을 받음, override한 가상 함수가 없음
{
public:
	explicit piano(int n) : trumpet(n) { }

};

int main()
{

	std::list<instrument*> li;

	trumpet test1(3);
	piano test2(4);

	li.push_back(&test1);			// 업 캐스팅의 경우 자동으로 수행
	li.push_back(&test2);			// 업 캐스팅의 경우 자동으로 수행	

	for (auto it : li)
	{
		if (trumpet* temp = dynamic_cast<trumpet*>(it))		// temp = nullptr을 반환 받지 않았다면
		{
			if (temp->stateGetter() >= 3)					// m_Trumpet_State의 값이 3이 넘을 때만
			{
				it->sound();
			}
		}
	}
	
}

 

출력 결과

trumpet 클래스의 가상 함수가 호출된 것을 볼 수 있다. 

 

상속 관계가 늘어날 경우 이를 유의하여서 사용하여야 합니다.