23. 상속(Inheritance)
CPP에서 클라스를 더욱 강력한 존재로 바꿔주는 개념인 상속은 실제 영어권에서는 { 계승 }이란 뜻으로 사용된다고 한다.
이름 그대로 자식 클라스가 부모 클라스를 계승해오는 개념으로 게임 개발 속도를 크게 올려준다.
이번엔 조금은 긴 코드를 통해서 이해해보자.
Private, Protected, Public
▼ 간단한(?) 클라스 상속 예시 코드
class Parent {
private:
int parentPrivateValue;
protected:
int parentProtectedValue;
public:
int parentPublicValue;
public:
Parent() {
parentPrivateValue = 1;
parentProtectedValue = 2;
parentPublicValue = 3;
printf("Parent Constructor Called\n");
};
~Parent()
{
printf("Parent Destructor Called\n");
};
void PrintPrivateAddress()
{
printf("PARENT[PrivateValue] : %p\n", &parentPrivateValue);
};
};
class Child : public Parent
{
private:
int childPrivateValue;
protected:
int childProtectedValue;
public:
int childPublicValue;
public:
Child()
{
childPrivateValue = 10;
childProtectedValue = 20;
childPublicValue = 30;
parentProtectedValue = 20;
parentPublicValue = 30;
printf("Child Constructor Called\n");
};
~Child()
{
printf("Child Destructor Called\n");
};
void PrintPrivateAddress()
{
printf("CHILD[PrivateValue] : %p\n", &childPrivateValue);
};
};
위의 코드에서 중점으로 봐야 하는 것은 { private, protected, public }의 차이다.
부모들도 사생활(Privacy)이 필요하고 이것까지 자식들에게 상속해줄 필요는 없다.
그래서 private로 선언된 변수들은 상속받는 자식들이 직접 접근할 수는 없다.
반면 protected와 public는 별 차이 없이 자식들이 사용할 수 있는 것처럼 보인다.
이 둘의 차이는 자식만 사용할 수 있는지 혹은 누구나 사용할 수 있는지의 차이가 있다.
CPP 클라스에서는 캡슐화를 지향하기 때문에 public을 사용하여 직접 값으로 접근하는 것은 권장되지 않는다.
함수 오버라이딩(Function Overriding)
이번엔 부모가 가지고 있는 함수를 자식이 똑같이 가지는 경우를 살펴본다.
기본적으로 같은 함수가 있다면 자식은 부모의 함수를 감추고 본인의 함수만 사용한다.
▼ 동일한 SetValue, GetValue 함수 사용
#include <cstdio>
#include <cstdlib>
class Parent {
private:
int parentPrivateValue;
public:
Parent() {
parentPrivateValue = 1;
printf("Parent Constructor Called\n");
};
~Parent()
{
printf("Parent Destructor Called\n");
};
void PrintPrivateAddress()
{
printf("PARENT[PrivateValue] : %p\n", &parentPrivateValue);
};
void SetValue(int _val)
{
printf("Parent SetValue Called\n");
parentPrivateValue = _val;
};
int GetValue()
{
printf("Parent GetValue Called\n");
return (parentPrivateValue);
};
};
class Child : public Parent
{
private:
int childPrivateValue;
public:
Child()
{
childPrivateValue = 10;
printf("Child Constructor Called\n");
};
~Child()
{
printf("Child Destructor Called\n");
};
void PrintPrivateAddress()
{
printf("CHILD[PrivateValue] : %p\n", &childPrivateValue);
};
void SetValue(int _val)
{
printf("Child SetValue Called\n");
childPrivateValue = _val;
};
int GetValue()
{
printf("Child GetValue Called\n");
return (childPrivateValue);
};
};
int main()
{
{
int num = 0;
Child child;
child.SetValue(50);
num = child.GetValue();
(static_cast<Parent>(child)).SetValue(50);
num = (static_cast<Parent>(child)).GetValue();
}
system("pause");
return (0);
}
같은 함수기 때문에 부모의 함수에 접근하기 위해서는 형변환(Type Casting)을 해줘야 한다.
추상화 프로그래밍
무기와 검 클라스를 통해서 간단한 상속의 응용법과 추상화에 대해서 알아본다.
문저 부모 클라스를 작성한다.
▼ 부모(Weapon) 클라스
///////////////////////////////////////////////
// Weapon.h
///////////////////////////////////////////////
#ifndef __WEAPON_H__
#define __WEAPON_H__
class Weapon
{
protected:
float melleeDamage;
int rank;
public:
Weapon(float _dmg, float _rank);
~Weapon();
void Attack();
};
#endif
///////////////////////////////////////////////
// Weapon.cpp
///////////////////////////////////////////////
#include "Weapon.h"
#include <cstdio>
Weapon::Weapon(float _dmg, float _rank)
{
melleeDamage = _dmg;
rank = _rank;
printf("Weapon Constructor Called\n");
}
Weapon::~Weapon()
{
printf("Weapon Constructor Called\n");
}
void Weapon::Attack()
{
printf("Weapon Attack Called\n");
}
부모 클라스는 추상적으로 작성할 수록 좋다.
어떤 무기던 공통적으로 가지는 속성들만 부모 클라스가 가지면 된다.
이를 { 추상화 프로그래밍 } 이라고 한다.
보다 더 추상화 프로그래밍을 연습하기 위해서 UML 다이어그램을 좀 더 살펴보면 좋다.
▼ 자식(Sword) 클라스
///////////////////////////////////////////////
// Sword.h
///////////////////////////////////////////////
#ifndef __SWORD_H__
#define __SWORD_H__
#include "Weapon.h"
class Sword : public Weapon
{
private:
public:
Sword(float _dmg, int _rank);
~Sword();
void Attack();
};
#endif
///////////////////////////////////////////////
// Sword.cpp
///////////////////////////////////////////////
#include "Sword.hpp"
#include <cstdio>
Sword::Sword(float _dmg, int _rank):
Weapon::Weapon(_dmg, _rank)
{
printf("Sword Constructor Called\n");
}
Sword::~Sword()
{
printf("Sword Constructor Called\n");
}
void Sword::Attack()
{
Weapon::Attack();
printf("Sword Attack Called\n");
}
운이 좋게도 자식(Sword) 클라스는 부모(Parent) 클라스와 크게 다르지 않다.
그래서 부모의 생성자와 함수를 그대로 사용해서 사용하고 있다.
▼ Main 함수
#include <cstdlib>
#include "Sword.hpp"
int main()
{
Sword* sword = new Sword(100.0f, 1);
sword->Attack();
if (sword != nullptr)
{
delete sword;
sword = nullptr;
}
system("pause");
return (0);
}
Sword* sword대신 Weapon* sword로 해도 Is-A 관계인가? 검증 문제를 잘 통과하기 때문에 문제없이 작동한다.
검은 무기가 되기 때문이다.
반대로 Sword* sword = new Weapon(100.0f, 1);은 문제가 발생한다.
무기가 검이 되지는 못하기 때문이다.
추상 클라스(Abstract Class)
이번엔 원거리 무기를 하나 만든다고 생각해보자.
원거리 무기는 탄약이 필요하기 때문에 자식(Gun) 클라스만 가지는 private 변수가 있다.
▼ 다른 자식(Gun) 클라스의 등장
///////////////////////////////////////////////
// Gun.h
///////////////////////////////////////////////
#ifndef __GUN_H__
#define __GUN_H__
#include "Weapon.h"
class Gun : public Weapon
{
private:
int mag;
public:
Gun(float _dmg, int _rank, int _mag);
~Gun();
void Attack();
};
#endif
///////////////////////////////////////////////
// Gun.cpp
///////////////////////////////////////////////
#include "Gun.hpp"
#include <cstdio>
Gun::Gun(float _dmg, int _rank, int _mag) :
Weapon::Weapon(_dmg, _rank)
{
mag = _mag;
printf("Gun Constructor Called\n");
}
Gun::~Gun()
{
printf("Gun Destructor Called\n");
}
void Gun::Attack()
{
printf("Gun Attack Called\n");
}
부모(Weapon)클라스와 Main 함수도 약간 수정한다.
▼ 부모(Weapon) 클라스 수정
///////////////////////////////////////////////
// Weapon.h
///////////////////////////////////////////////
#ifndef __WEAPON_H__
#define __WEAPON_H__
class Weapon
{
protected:
float melleeDamage;
int rank;
public:
Weapon(float _dmg, float _rank);
virtual ~Weapon();
// Virtual Function Table
// 순수 가상 함수(Pure Virtual Function)
virtual void Attack() = 0;
};
#endif
///////////////////////////////////////////////
// Weapon.cpp
///////////////////////////////////////////////
#include "Weapon.h"
#include <cstdio>
Weapon::Weapon(float _dmg, float _rank)
{
melleeDamage = _dmg;
rank = _rank;
printf("Weapon Constructor Called\n");
}
Weapon::~Weapon()
{
printf("Weapon Destructor Called\n");
}
아예 Attack 함수를 가상함수(Virtual Function)로 변경했다.
순수 가상함수를 가지고 있기 때문에 객체로 만들 수 없다. 이것을 순수 가상 클라스라고 한다.
▼ Main 함수 수정
#include "Sword.hpp"
#include "Gun.hpp"
int main()
{
{
Weapon* weapon = new Gun(100.0f, 1, 8);
weapon->Attack();
if (weapon != nullptr)
{
delete weapon;
weapon = nullptr;
}
}
system("pause");
return (0);
}
다형성(Polymorphizm)
▼ 무엇을 빼내오던 해제는 Weapon으로 하면 된다.
#include "Sword.hpp"
#include "Gun.hpp"
enum class EWeapon { SWORD, GUN };
Weapon* WeaponFactory(EWeapon _weapon)
{
switch (_weapon)
{
case EWeapon::SWORD:
return new Sword(100.0f, 10);
break;
case EWeapon::GUN:
return new Gun(50.0f, 15, 8);
break;
}
}
int main()
{
{
//Weapon* weapon = new Gun(100.0f, 1, 8);
Weapon* weapon = WeaponFactory(EWeapon::GUN);
weapon->Attack();
if (weapon != nullptr)
{
delete weapon;
weapon = nullptr;
}
}
system("pause");
return (0);
}
이 예시는 다형성(Polymolphism)을 이용한 예시로 { 팩토리 패턴 }의 대표적인 모습이다.
게임 개발에서 많이 사용하는 디자인 패턴 중 하나로 코드를 극적으로 줄일 수 있다.
게임에서 사용되는 패턴은 한정적이기 때문에 CPP를 공부할 때 같이 하면 좋다.