본문 바로가기
강좌연구

[C++ 강좌연구] 동적 바인딩

by 두들낙서 2019. 6. 18.

 

다음 강의 일주일 내로 올라갑니다. 오래 기다리게 해서 죄송합니다.

 


 

다음 코드는 어떤 게임의 일부이다. 무기의 종류에는 칼(Sword)과 마법(Magic)이 있고, Sword와 Magic이라는 두 클래스가 Weapon 클래스에서 상속을 받아 구현한 상태다. main 안의 currentWeapon은 플레이어가 현재 들고 있는 무기를 가리키고 있다.

#include <iostream>

using namespace std;

class Weapon {
public:
	Weapon(int power) : power(power) {
		cout << "Weapon(int)" << endl;
	}

protected:
	int power;
};

class Sword : public Weapon {
public:
	Sword(int power) : Weapon(power) {
		cout << "Sword(int)" << endl;
	}

	void Use() {
		cout << "Sword::Use()" << endl;
		Swing();
	}

private:
	void Swing() {
		cout << "Swing sword." << endl;
	}
};

class Magic : public Weapon {
public:
	Magic(int power, int manaCost) : Weapon(power), manaCost(manaCost) {
		cout << "Magic(int, int)" << endl;
	}

	void Use() {
		cout << "Magic::Use()" << endl;
		Cast();
	}

private:
	void Cast() {
		cout << "Cast magic." << endl;
	}

	int manaCost;
};

int main() {
	Sword mySword(10);
	Magic myMagic(15, 7);

	mySword.Use(); // OK
	myMagic.Use(); // OK

	Weapon *currentWeapon; // 현재 무기를 포인터로 가리키자.

	currentWeapon = &mySword;
	currentWeapon->Use(); // 에러!
}

mySword와 myMagic은 Use 멤버 메서드를 문제 없이 호출할 수 있다.

문제는 currentWeapon이다. 이 친구는 mySword를 가리킬 수도 있고, myMagic을 가리킬 수도 있다. 그러나 두 클래스 공통으로 들어있는 Use 함수를 호출할 수는 없다. 어디까지나 부모 클래스(Weapon)의 객체를 가리키는 포인터이기 때문이다. (Weapon 클래스에는 Use 메서드가 없다.)

오버라이딩을 배웠다면 다음과 같이 Use를 Weapon 클래스에도 만들어서 Sword와 Magic에서 이 함수를 오버라이딩하는 형태로 만들어 보려는 시도를 할 수 있다.

class Weapon {
public:
	Weapon(int power) : power(power) {
		cout << "Weapon(int)" << endl;
	}

	void Use() {
		cout << "Weapon::Use()" << endl;
	}

protected:
	int power;
};

이제 컴파일 에러는 나지 않는다. 하지만 정적 바인딩의 내용을 잘 생각해보면, 자식 클래스가 부모 클래스의 함수를 오버라이딩해도, 부모 포인터가 자식을 가리키는 경우, 자식이 아닌 부모가 호출이 된다.

즉, currentWeapon->Use()를 호출하면 우리는 currentWeapon이 현재 가리키고 있는 무기의 종류에 따라 "동적으로" Sword::Use()를 호출하든가, Magic::Use()를 호출했으면 좋겠지만, 실제로는 그에 상관없이 항상 Weapon::Use()가 호출이 된다. 즉 정적 바인딩이 일어난다. (정적 바인딩을 잘 공부했다면 무슨 뜻인지 알 것이다.)

이 상황을 해결할 수 있는, 즉 currentWeapon을 통해 자식 클래스의 Use 함수를 호출할 수 있는 방법이 없다면, 상속을 하는 의미도 없어진다.

 

우리가 원하는 걸 명확히 해보자.

currentWeapon은 Weapon을 가리키는 포인터이고,

이 친구가 Sword 객체를 가리키고 있을 때는 Sword::Use()가,

Magic 객체를 가리키고 있을 때는 Magic::Use()가 호출되게끔 하고 싶다.

 

이 문제는 "동적 바인딩"을 통해 쉽게 해결할 수 있다. 동적 바인딩이란 바로 위에 써놓은 것과 완전 같은 의미이다. 추상적으로 말하면, 자식 클래스가 부모 클래스의 함수를 오버라이딩한 경우, 자식 클래스의 객체를 가리키고 있는 부모 포인터를 통해 자식 클래스의 함수에 접근하는 것이 바로 동적 바인딩이다.

이런 거창한(?) 개념에 비해 구현은 굉장히 단순하다. virtual 키워드를 사용해 "가상함수"를 만들어 주면 된다. 나머지는 그대로 놔둬도 된다.

class Weapon {
public:
	Weapon(int power) : power(power) {
		cout << "Weapon(int)" << endl;
	}

	virtual void Use() {
		cout << "Weapon::Use()" << endl;
	}

protected:
	int power;
};

이제 main 함수에서 currentWeapon->Use()를 호출하면 놀랍게도 Sword::Use()가 호출된다.

부모 클래스에 가상함수만 만들어줬을 뿐인데 정적 바인딩에서 동적 바인딩으로 바뀌었다. 가상함수 덕분에 상속을 받은 클래스들에게 좀더 '특화된' 기능을 부여할 수 있다. 같은 Use 함수를 통해 호출하지만, 칼(Sword)일 경우 Swing() 함수가 호출되고, 마법일 경우 Cast() 함수가 호출된다. 실제 게임이 아닌지라 함수의 내용을 구현하지는 않았지만, 클래스별로 특화된 Use 함수를 통해 무기 종류별로 그래픽 효과나 능력치 변화 등등을 상황에 알맞게 줄 수 있다.

 

또 당연한 소리일지도 모르겠지만 만약 자식 클래스가 Use를 애초에 오버라이딩하지 않았다면 virtual 키워드가 붙었다 하더라도 부모의 Use 함수가 호출된다.

#include <iostream>

using namespace std;

class Weapon {
public:
	Weapon(int power) : power(power) {
		cout << "Weapon(int)" << endl;
	}

	virtual void Use() {
		cout << "Weapon::Use()" << endl;
	}

protected:
	int power;
};

class Sword : public Weapon {
public:
	Sword(int power) : Weapon(power) {
		cout << "Sword(int)" << endl;
	}

	// Use()가 오버라이딩되지 않았다.
};

int main() {
	Sword mySword(10);
	Weapon *currentWeapon = &mySword;
	currentWeapon->Use(); // Weapon::Use()가 호출된다.
}


'강좌연구' 카테고리의 다른 글

[C++ 강좌연구] 정적 바인딩  (0) 2019.02.02
[C++ 강좌연구] 오버라이딩  (0) 2019.02.02

댓글