다형성에 관하여 1 - 업캐스팅, 다운캐스팅, 정적타입, 동적타입 -
요약
업캐스팅: 파생 클래스의 객체를 기본 클래스의 포인터로 가리키는 것
다운캐스팅: 기본 클래스 포인터가 가리키는 객체를 파생 클래스의 포인터로 가리키는 것
정적 타입 (Static Type) - 포인터가 선언될 때의 타입
동적 타입 (Dynamic Type) - 포인터가 실행 중에 가리키고 있는 대상체의 타입
“부모는 자식을 가리킬 수 있다.”
주제
저번 포스트에 추상화, 상속성, 다형성 개념을 정리하고 게임을 만들며 실습해봤다.
이번엔 작동 원리를 공부해보자.
부모와 자식 클래스 객체 간 타입 캐스팅은 어떻게 이루어지는지 알아보자.
추상화, 상속
먼저 기본 클래스 Unit과 파생 클래스 Monster 를 다음과 같이 만들고자 한다.
#include <iostream>
#include <vector>
using namespace std;
class Unit
{
protected:
string name;
int hp;
int atk;
public:
Unit() = default;
Unit(string _name, int _hp, int _atk) :
name(_name), hp(_hp), atk(_atk) {}
~Unit() {}
public:
void PrintInfo()
{
cout << "name: " << name << '\n';
cout << "hp: " << hp << '\n';
cout << "atk: " << atk << '\n';
}
};
class Monster : public Unit // 상속
{
private:
int grade;
public:
Monster() = default;
Monster(string _name, int _hp, int _atk, int _monsterGrade) :
Unit(_name, _hp, _atk), grade(_monsterGrade) {}
~Monster() {}
public:
void PrintInfo()
{
Unit::PrintInfo();
cout << "grade: " << grade << '\n';
}
};
enum eMonsterGrade { normal, chief, boss };
int main()
{
Unit unit("유닛", 10, 1);
Monster monster("몬스터", 50, 5, normal);
unit.PrintInfo();
cout << '\n';
monster.PrintInfo();
}
name: 유닛
hp: 10
atk: 1
name: 몬스터
hp: 50
atk: 5
grade: 0
그 결과 다음과 같이 생성될 것이다.
객체와 포인터
갑자기 변덕이 든다.
Unit 타입이었던 애를 몬스터처럼 바꾸고 싶다.
Monster 타입이었던 애를 기본 유닛처럼 바꾸고 싶다.
상속 관계의 클래스 간에는 포인터로 서로 가리킬 수 있지 않을까?
객체를 가리키는 포인터를 만들어
부모는 자식을, 자식은 부모를 가리키도록 만들어 보자.
#include <iostream>
#include <vector>
using namespace std;
class Unit
{
protected:
string name;
int hp;
int atk;
public:
Unit() = default;
Unit(string _name, int _hp, int _atk) :
name(_name), hp(_hp), atk(_atk) {}
virtual ~Unit() {}
public:
virtual void PrintInfo()
{
cout << "name: " << name << '\n';
cout << "hp: " << hp << '\n';
cout << "atk: " << atk << '\n';
}
};
class Monster : public Unit // 상속
{
private:
int grade;
public:
Monster() = default;
Monster(string _name, int _hp, int _atk, int _monsterGrade) :
Unit(_name, _hp, _atk), grade(_monsterGrade) {}
~Monster() override {}
public:
void PrintInfo() override
{
Unit::PrintInfo();
cout << "grade: " << grade << '\n';
}
};
enum eMonsterGrade { normal, chief, boss };
int main()
{
Unit unit("유닛", 10, 1);
Monster monster("몬스터", 50, 5, normal);
Unit* pUnit;
Monster* pMonster;
pUnit = &unit; // 당연히 가능
pMonster = &monster; // 당연히 가능
pUnit = &monster; // 가능. 업캐스팅
pUnit->PrintInfo(); // 호출
cout << '\n';
// pMonster = &unit; // 에러
pMonster = (Monster*)&unit; // 가능. 다운캐스팅
pMonster->PrintInfo(); // 호출
}
name: 몬스터
hp: 50
atk: 5 // grade가 나오지 않았다.
name: 유닛
hp: 10
atk: 1
grade: -858993460 // grade가 쓰레기값으로 초기화 되었다.
어라.. 의문점이 2가지나 생겼다.
- 자식 객체가 부모 객체를 가리키지 못한다.
- 포인터로 가리킨 타입이 아닌 처음 선언된 타입의 함수를 호출해버린다.
업캐스팅, 다운캐스팅
첫 번째 의문점은
pUnit = &monster; // 가능
는 잘 되는데
// pMonster = &unit; // 에러
는 별도로 타입 캐스팅을 해주어야 한다는 점이다.
업캐스팅: 파생 클래스의 객체를 기본 클래스의 포인터로 가리키는 것
pUnit = &monster; // 업캐스팅
다운캐스팅: 기본 클래스 포인터가 가리키는 객체를 파생 클래스의 포인터로 가리키는 것
pMonster = &pUnit; // 에러
pMonster = (Monster*)&unit; // 다운캐스팅. 명시적 타입 변환 필요
C++에서는 상속 관계의 클래스 간 대입 시 좌변이 더 상위의 클래스 타입이면 캐스팅을 하지 않고 직접 대입이 허용된다.
즉, 부모는 자식을 가리킬 수 있다.
반면 좌변이 하위의 클래스일 경우 명시적 타입 캐스팅을 통해 가리켜야 한다.
정적 타입 vs 동적 타입
두 번째 의문점은 다음과 같다.
- pUnit은 monster를 가리켰지만, unit의 PrintInfo 함수를 호출했다.
- pMonster는 unit을 가리켰지만, monster의 PrintInfo 함수를 호출했다.
정적 타입 (Static Type) - 포인터가 선언될 때의 타입
동적 타입 (Dynamic Type) - 포인터가 실행 중에 가리키고 있는 대상체의 타입
pUnit = &unit; // 정적 타입
pUnit = &monster; // 동적 타입
C++에선 포인터가 선언될 때의 정적 타입을 보고 이 타입에 맞는 함수를 호출한다.
그렇다면 어떻게 가리키고 있는 타입(동적 타입)에 따라 멤버 함수가 선택되도록 할 수 있을까?
이제서야 가상 함수가 등장한다.
다음 포스트에서 가상 함수의 원리를 공부하며 어떻게 동적 타입에 따라 멤버 함수가 선택될 수 있는지 알아보자.
참고
※ 틀리거나 개선할 부분이 있다면 알려주세요.
댓글남기기