C++/C++ : Study

다형성(2) - 동적 바인딩 & 가상 함수

더블유제이플로어 2025. 5. 1. 19:56

Polymorphism

◆  동적 바인딩의 예시 1

	Entity entity{ 0,0 };
	entity.Move(1, 1); // Entity::Move()

	Player player{ 0,0,2 };
	player.Move(1, 1); //Player::Move()

	Boss boss{ 0,0,2 };
	boss.Move(1, 1); //Boss::Move()

	Entity* ePtr = new Boss{ 0,0,2 };
	ePtr->Move(1, 1); // Boss::Move() => 동적 바인딩

 

* 컴파일러는 런타임에 ePtr의 실제 타입을 확인하고, 해당 클래스의 멤버 함수를 호출함

ePtr에 있는 Move() 함수를 호출할때
  정적바인딩인 경우 : 객체 타입인 Entity* 를 보고 함수를 확인
  동적 바인딩인 경우 : 실제로 메모리에 올라가 있는 Move() 함수가 호출된다.


◆  동적 바인딩의 예시 2

void DisplayPosition(const Entity& e)
{
	e.ShowPosition(); // 각자의 ShowPosition()
}

Entity entity{ 0,0 };
DisplayPosition(entity); 

Player player{ 0,0,2 };
DisplayPosition(player);
    
Enemy enemy{ 0,0,2 };
DisplayPosition(enemy);

* 컴파일러는 런타임 e의 타입을 확인하고, 해당 클래스의 멤버 함수를 호출함

함수의 인자로 객체를 넘기는 경우에도 마찬가지이다.
Entity 객체, Player 객체, Enemy 객체 혹은 Boss 객체 인자 중 어떤 인자를 넘겨주는지 따라 Move() 함수의 클래스가 달라진다.

정적 바인딩인 경우
const Entity& e 일때 어떤 인자가 오더라도 무조건 Entity 타입의 Move() 함수가 호출이 된다.
동적 바인딩인 경우
런타임에서 타입을 확인 하고 Move() 함수를 실행한다.

정적 바인딩과 동적 바인딩은 객체 불러오는 코드는 변함이 없다.
그렇지만 동적 바인딩을 하게 되면 다른 함수가 호출될 수 있다.

동적 바인딩을 통해 같은 코드로 다양한 객체의 함수를 호출 할 수 있기 때문에  "다형성(Polymorphism)" 이라는 이름이 붙었다.


Polymorphism

◆  다형성과 동적 바인딩 : 기본 동작인 정적 바인딩은 컴파일 시 타입을 기준으로 호출 함수를 결정하지만, 동적 바인딩을 하게 되면 런타임 시 실제 메모리에 저장된 타입을 기준으로 호출 함수를 결정
◆  가상 함수
◆  기본 클래스의 포인터 / 참조자
◆  override / final 지정자
◆  순수 가상 함수와 추상 클래스
◆  추상 클래스와 인터페이스

정적 바인딩: 컴파일 시 타입을 기준으로 호출 함수를 결정
동적 바인딩: 실제 메모리에 저장된 타입을 기준으로 호출 함수를 결정


Virtual Function

◆  가상 함수

   ●  Move() 와 같이, 유도 클래스에서 기본 클래스의 함수를 재정의 또는 오버라이드해 사용할 수 있다.
   ●  오버라이드 된 함수를 가상 함수라고 한다.
        ☞  기본 클래스의 함수(Entity::Move)가 가상 함수로 선언 후,
        ☞  유도 클래스에서 해당 함수를 오버라이드해서 구현하면 동적 바인딩 된다.

◆  C++ 에서 런타임 다형성의 구현을 위해 3가지 조건이 필요하다.

   ●  상속
   ●  기본 클래스 포인터 또는 참조자
   ●  가상 함수

가상함수 (virutal Function) 
동적 바인딩을 하려면 3가지 조건이 만족해야 한다.
1. 상속 2. 기본 클래스 포인터 또는 참조자 3. 가상함수
세 번째 가상함수여야지만 동적 바인딩을 할 수 있다.

가상함수는 Move() 함수와 같이 유도 클래스에서 기본 클래스의 함수를 재정의 혹은 오버라이드하여 사용할 수 있다.
함수를 오버라이드 하게 되면은 동적 바인딩이 된다.
오버라이드 될 수 있는 함수를 가상 함수로 지정해 준다.
기본 클래스에 Move() 함수가 있고, 그 Move() 함수를 가상함수로 선언해 주면
유도 클래스에서 해당 Move() 함수를 오버라이드하여 구현하면 동적 바인딩이 된다.

정적 바인딩에서는 이렇게 구성되어있는것을 동적 바인딩으로 바꿔보겠다.

Entity::Move() 함수를 가상함수로 바뀌게 되면 Player::Move() 함수가 Entity::Move() 함수를 덮어씌우게 된다.

덮어쓰도록 오버라이딩을 할 수 있게 된다.
그래서 오버라이딩 한다는 의미는 유도 클래스 함수가 기본 클래스 함수를 덮어쓰는 걸 이야기하는 것이고, 가상함수는 덮어 쓰일 수 있도록 Entity에 있는 Move() 함수를 가상함수로 선언해줘야 한다는 의미이다.
오버라이딩은 어떤 단어인지, 어떤 의미인지 좀 더 명확히 하기 위해서 설명을 하였다.


Virtual Function

◆  가상 함수의 선언, 기본 클래스에서 할 일

   ●  오버라이드 할 함수를 기본 클래스에서 virtual로 선언.
   ●  상속 계층 구조에 있는 모든 해당 함수는 가상 함수가 된다.

class Entity{
public:
   virtual void Move(int dx, int dy);
   ...
}
기본 클래스의 멤버 함수 앞에 virual 키워드를 붙이면, 가상 함수가 된다.

구현 방법은 간단하다. 

#include <iostream>
using namespace std;

class Entity {
private:
	int x;
	int y;
public:
	Entity(int x, int y)
		:x{ x }, y{ y } { } 
	void Move(int dx, int dy) {
		x += dx;
		y += dy;
	}
	void PrintPosition()
	{
		cout << x << ", " << y << endl;
	}
};
int main()
{
	Entity e{ 1,1 };
	e.PrintPosition(); // 1, 1

	e.Move(2, 1);
	e.PrintPosition(); // 3, 2
}

Entity를 상속하는 Player 클래스를 만들어보자.

#include <iostream>
using namespace std;

class Entity {
protected: // private 에서 변경
	int x;
	int y;
public:
	Entity(int x, int y)
		:x{ x }, y{ y } { }
	void Move(int dx, int dy) {
		x += dx;
		y += dy;
	}
	void PrintPosition()
	{
		cout << x << ", " << y << endl;
	}
};

class Player : public Entity {
private:
	int hp;
public:
	Player(int x, int y, int hp)
		:Entity{x,y},hp{hp} { }
	void Move(int dx, int dy)
	{
		x += dx * 2;
		y += dy * 2;
	}
};

int main()
{
	Player e{ 1,1,10 };
	e.PrintPosition(); // 1,1

	e.Move(2, 1);
	e.PrintPosition(); // 5,3

동적바인딩을 해줄려면 기본 클래스의 함수를 가상함수로 만들어주어야 한다.
void Move() 앞에 virtual을 넣어 virtual void Move()로 선언해 주는 것이다.

기본 클래스에 있는 함수가 가상함수가 되었다는 뜻은 유도클래스에서 override 가 될 수 있는 상태가 되었다는 의미이다.

override를 하는 방법은 가상함수의 이, 인자, 반환형이 모두 같아야 한다.
Entity::virtual void Move(int dx, int dy);
Player::void Move(int dx, int dy);

만일 유도 클래스와 가상함수가 인자가 다를 경우 override 하는 함수가 아니다.
반환형, 이름, 인자가 다를 경우에는 override을 할 수 없다.

Player::virtual void Move(int dx, int dy);
유도 클래스에 Move() 함수는 virtual 키워드를 넣나 안 넣나 별 차이는 없다.
하지만 virtual 키워드를 써놔야 추후 다른 개발자가 유추할 수 있다.


중요한 Part이다.

◆  가상 함수의 선언, 유도 클래스에서 할 일

   ●  오버라이드 할 함수를 유도 클래스에서 구현
   ●  함수 원형 (prototype)과 반환형이 기본 클래스의 가상 함수와 일치해야 한다.
   ●  유도 클래스의 함수에서는 virtual 키워드를 넣지 않아도 되지만, 혼동을 피하기 위해 명시해 주는 것을 권고
   ●  유도 클래스에서 함수를 오버라이드 하지 않으면, 기존과 같이 기본 클래스의 함수가 상속된다.

class Player : public Entity{
public:
  virtual void Move(int dx, int dy);
  ...
}

#include <iostream>
using namespace std;

class Entity {
protected: // private 에서 변경
	int x;
	int y;
public:
	Entity(int x, int y)
		:x{ x }, y{ y } { }
	void Move(int dx, int dy) {
		x += dx;
		y += dy;
	}
	void PrintPosition()
	{
		cout << x << ", " << y << endl;
	}
};

class Player : public Entity {
private:
	int hp;
public:
	Player(int x, int y, int hp)
		:Entity{x,y},hp{hp} { }
};

int main()
{
	Player e{ 1,1,10 };
	e.Move(2, 1); // ??

유도 클래스에 Move() 함수가 없을 경우에
e.Move(2,1); 은 어떻게 될까?
디버깅을 해보면 기본 클래스 Entity::Move(int , int)로 들어간다.


중요한 Part이다.

Virtual Destructor

◆  가상 소멸자

   ●  다형성 객체를 소멸할 때의 고려사항
        ☞  포인터를 사용한 뒤 해제할 때, 소멸자가 정적 바인딩되어 있다면 기본 클래스의 소멸자가 호출

class Entity {
private:
	int x;
	int y;
public:
	Entity(int x, int y)
		:x{ x }, y{ y } { }
	~Entity()
	{
		cout << "Entity Destructor" << endl;
	}
};

class Player : public Entity {
private:
	int hp;
public:
	Player(int x, int y, int hp)
		:Entity{x,y},hp{hp} { }
	~Player()
	{
		cout << "Player Destructor" << endl;
	}
};
int main()
{
	Entity* ptr = new Player{ 2,3,5 };
	// use ptr
	delete ptr;
}
ptr을 해제하면 소멸자가 호출된다.
ptr의 타입은 Entity* 이므로 Entity의 소멸자만 호출한다.
만일 Player의 멤버에 동적 할당한 포인터가 있었다면 어떤 문제가 발생할까?

동적바인딩을 사용할때 코드에 포인터가 있을 경우
소멸자를 꼭 구현했다고 가정해 보자.
소멸자도 virtual 키워드를 붙여 가상 소멸자로 만들어줘야 한다.
그렇게 안할 경우 문제가 생긴다.

#include <iostream>
using namespace std;

class Entity {
protected:
	int x;
	int y;
public:
	Entity(int x, int y)
		:x{ x }, y{ y } { }
	virtual ~Entity()
	{
		cout << "Entity Destructor" << endl;
	}
	virtual void Move(int dx, int dy) {
		x += dx;
		y += dy;
	}
	void PrintPosition()
	{
		cout << x << ", " << y << endl;
	}
};

class Player : public Entity {
private:
	int hp;
public:
	Player(int x, int y, int hp)
		:Entity{x,y},hp{hp} { }
	void Move(int dx, int dy)
	{
		x += dx * 2;
		y += dy * 2;
	}
	~Player()
	{
		cout << "Player Destructor" << endl;
	}
};

int main()
{
	Entity* ptr = new Player{ 2,3,5 };
	delete ptr;
    
    // Player Destructor
    // Entity Destructor 콘솔 입력됨
}

Player 소멸자, Entity 소멸자 모두 호출이 된다.

만약에 기본 소멸자를 virtual를 안 붙이게 되면,

#include <iostream>
using namespace std;

class Entity {
protected:
	int x;
	int y;
public:
	Entity(int x, int y)
		:x{ x }, y{ y } { }
	~Entity()
	{
		cout << "Entity Destructor" << endl;
	}
	virtual void Move(int dx, int dy) {
		x += dx;
		y += dy;
	}
	void PrintPosition()
	{
		cout << x << ", " << y << endl;
	}
};

class Player : public Entity {
private:
	int hp;
public:
	Player(int x, int y, int hp)
		:Entity{x,y},hp{hp} { }
	void Move(int dx, int dy)
	{
		x += dx * 2;
		y += dy * 2;
	}
	~Player()
	{
		cout << "Player Destructor" << endl;
	}
};

int main()
{
	Entity* ptr = new Player{ 2,3,5 };
	delete ptr;
    
    // Entity Destructor 콘솔 입력됨
}

동적바인딩을 사용할때 코드에 포인터가 있을 경우
소멸자를 꼭 구현했다고 가정해 보자.
소멸자도 virtual 키워드를 붙여 가상 소멸자로 만들어줘야 한다.
그렇게 안할 경우 문제가 생긴다.

Player 소멸자, Entity 소멸자 모두 호출이 된다.
만약에 기본 소멸자를 virtual를 안 붙이게 되면,
Entity 소멸자만 호출이 된다.

delete e; 처럼(?) 함수를 해체하게 된다면
소멸자는 자동으로 호출이 된다.
자동으로 호출할 소멸자를 "타입"을 기준으로 판단하게 된다.
e는 Entity* 타입이기 때문에 Entity 소멸자를 호출하는 것이다.

예를 들어 문제가 Player 클래스에서
private:
int* hp;
일 경우에 문제가 생긴다.
Player 소멸자에서 hp를 해제를 해주었을 텐데
지금 virtual 을 쓰지 않는 코드에서는 Player 소멸자가 호출되지 않는다.

그래서 Player 소멸자가 제대로 호출되게 만들려고 한다면
기본클래스에 있는 소멸자를 가상 함수로 만들어 줘야 한다.
virtual 키워드를 앞에 작성하는 것이다.
기본 클래스에서 가상함수를 만들었듯이 유도 클래스에서도 함께 가상함수를
만들어주는 게 좋다.

#include <iostream>
using namespace std;

class Entity {
protected:
	int x;
	int y;
public:
	Entity(int x, int y)
		:x{ x }, y{ y } { }
	virtual ~Entity()
	{
		cout << "Entity Destructor" << endl;
	}
	virtual void Move(int dx, int dy) {
		x += dx;
		y += dy;
	}
	void PrintPosition()
	{
		cout << x << ", " << y << endl;
	}
};

class Player : public Entity {
private:
	int hp;
public:
	Player(int x, int y, int hp)
		:Entity{x,y},hp{hp} { }
	void Move(int dx, int dy)
	{
		x += dx * 2;
		y += dy * 2;
	}
	virtual ~Player()
	{
		cout << "Player Destructor" << endl;
	}
};

int main()
{
	Entity* ptr = new Player{ 2,3,5 };
	delete ptr;
    
    // Player Destructor
    // Entity Destructor 콘솔 입력됨
}

중요한 Part이다.

◆  가상 소멸자

   ●  유도 객체를 올바른 순서로, 올바른 소멸자를 사용해 소멸시키는 방법이 필요하다.
   ●  해결 방법? -> 가상 소멸자
        ☞  클래스가 가상 함수를 가지면, 항상 소멸자를 함께 정의해야 한다.
        ☞   마찬가지로, 기본 클래스의 소멸자가 가상 소멸자면, 유도 클래스의 소멸자도 가상 소멸자이다.

class Entity{
public:
   virtual ~Entity();
   ...
};
앞장의 코드에서 Entity의 소멸자를 가상으로 선언해 주면 Player의 소멸자와 Entity의 소멸자가 모두 호출된다.
Player의 소멸자만 호출되는 것이 아닌 이유는?

소멸자는 헷갈릴 수 있으나 강의를 끝까지 듣고 나서 이해를 하면
조금 더 명확히 이해가 될 수 있다.

기본 클래스의 소멸자가 가상함수가 아니면
유도 클래스의 소멸자가 호출이 안된다.

클래스가 가상 함수를 가지면, 항상 가상 소멸자를 정의해야한다.
그래야 소멸자가 제대로 호출이 된다.


Polymorphism

◆  다형성과 동적 바인딩 : 기본 동작인 정적 바인딩은 컴파일 시 타입을 기준으로 호출 함수를 결정하지만, 동적 바인딩을 하게 되면 런타임 시 실제 메모리에 저장된 타입을 기준으로 호출 함수를 결정
◆  가상 함수 : 컴파일러에게 동적 바인딩을 하려고 알려주기 위해서는 기본 클래스의 함수를 virtual 키워드를 통해 가상 함수로 만들어주어야 한다. 이런 경우, 소멸자도 항상 가상 함수로 선언 필요하다.
◆  기본 클래스의 포인터 / 참조자 
◆  override / final 지정자
◆  순수 가상 함수와 추상 클래스
◆  추상 클래스와 인터페이스


컴파일러에게 동적 바인딩을 하라고 명령하기 위해서는 3가지 조건이 필요한데,
그중 하나는 기본 클래스의 함수를 가상함수여야 한다.
가상함수를 선언하는 방법은 함수 앞에 virtual을 쓰면 된다.
그럴 경우에 소멸자도 항상 가상 함수로 선언되어야 한다.


https://youtu.be/hAkOCT9Z-oc?si=FliCe3DUf25yjQDE

핑계이지만, 해외 숙소를 옮기면서 책상이 없어지면서 21분 강의를 듣는데 시간이 많이 걸렸다.

곧 책상 있는 방으로 옮길 예정이니 그때는 힘차게 공부하여 5월 안에는 상속을 다 마무리하였으면 좋겠다.

'C++ > C++ : Study' 카테고리의 다른 글

다형성 (4) - overrider & Final 지정자  (0) 2025.05.18
다형성 (3) - 기본 클래스 포인터 & 참조자  (0) 2025.05.01
다형성(1) - 소개  (0) 2025.04.27
상속(8) - 멤버의 사용  (0) 2025.04.17
상속(7) - 복사 생성자  (1) 2025.04.02