Introduce
객체 지향 프로그래밍의 주요 특성 중 하나인 다형성(polymorphism)에 대한 개념을 이해한다.
다형성
다형성(polymorphism)은 여러가지 형태를 갖는 성질을 말한다. 함수의 기능적 다형성과 상속에 의해 만들어진 계층 관계를 활용하는 기술이다.
오버로딩
오버로딩은 같은 이름의 함수에 매개변수를 다르게 사용하여 매개 변수에 따라 다른 함수가 실행되는 것 입니다.
오버로딩 규칙
- 메소드 이름이 같아야 한다.
- 리턴형이 같아도 되고 달라도 된다.
- 파라미터 개수가 달라야한다.
- 파라미터 개수가 같을 경우, 자료형이 달라야 한다.
class Knight
{
public:
int _hp;
int _attack;
int _posX;
int _posY;
public:
// 기본 생성자.
Knight()
{
cout << "Knight() Constructor" << endl;
_hp = 100;
_attack = 10;
_posX = 0;
_posY = 0;
}
// 기타 생성자 (매개변수 있다.)
Knight(int hp)
{
cout << "Knight (int hp) Constructor" << endl;
_hp = hp;
_attack = 10;
_posX = 0;
_posY = 0;
}
// 복사 생성자
Knight(const Knight &Knight)
{
_hp = Knight._hp;
_attack = Knight._attack;
_posX = Knight._posX;
_posY = Knight._posY;
}
~Knight() { cout << "Knight() Destructor" << endl; }
void Move(int x, int y);
void Attack();
void Die()
{
_hp = 0;
cout << "Dead" << endl;
}
};
생성자를 매개변수에 따라 여러가지 버전으로 선언한 것도 오버로딩의 예입니다.
오버라이딩
오버라이딩은 상속받았을때 부모 클래스의 함수를 사용하지 않고 다른 기능을 실행할때 함수를 자식클래스에 같은 이름, 매개변수를 재정의해서 사용하는 것입니다.
오버라이딩 규칙
- 오버라이드 하고자 하는 메소드가 상위 클래스에 존재해야한다.
- 메소드 이름이 같아야한다.
- 메소드 파라미터 개수, 파라미터의 자료형이 같아야 한다.
- 메소드 리턴형이 같아야 한다.
- 상위 메소드와 동일하거나 내용이 추가되어야 한다.
#include <iostream>
using namespace std;
class Player
{
public:
int _hp;
public:
Player()
{
_hp = 100;
cout << "Player Constructer" << endl;
}
void Move()
{
cout << "Move Player" << endl;
}
};
class Knight : public Player{
public:
int _stamina;
public:
Knight(){
_hp = 100;
cout << "Knight Constructer" << endl;
}
void Move(){
cout << "Move Knight" << endl;
}
};
int main(){
Knight k;
k.Move();
return 0;
}
===== 실행 결과 =====
Player Constructer
Knight Constructer
Move Knight
Player의 멤버 함수 Move를 상속받은 Knight 객체에서 재정의해 오버라이딩한 경우입니다.
바인딩
바인딩(binding)이란 프로그램 소스에 쓰인 각종 내부 요소, 이름 식별자들에 대해 값 또는 속성을 확정하는 과정을 말합니다.
정적바인딩
바인딩 과정이 컴파일 시점에 이루어지는 것을 정적 바인딩이라 합니다. 밑의 예시처럼 소스상에 명시적으로 타입과, 그 타입의 변수명을 선언하는 것을 정적 바인딩이라합니다.
int main(){ int a = 0; }
정적 바인딩은 컴파일시 타입에 대한 정보가 결정되기 때문에 속도가 빠르며, 타입에러 문제에 대해 안정적입니다. 하지만 컴파일시 결정이 되고 그 이후는 변경이 불가능하다는 단점이 있습니다.
// 모든것을 최상위 class로 뭉퉁그려서 상속받는 애들까지 한꺼번에 맞춰 관리할 수 있다.
void MovePlayer(Player *Player){
Player->Move();
}
void MoveKnight(Knight *knight){
knight->Move();
}
int main(){
Player p;
MovePlayer(&p);
MoveKnight(&p); // knight에 player를 넘겨줘서 호환이 안된다.
Knight k;
MoveKnight(&k);
MovePlayer(&k); // 반대로 player에 knight 넘겨주는 것은 호환이 된다.
return 0;
}
Player와 Knight가 상속관계라 한다면 이 특징을 이용해 어떤 함수에 매개변수로 넘겨줄때 이점을 볼 수 있습니다.
MovePlayer()에 knight의 주소를 넘겨주는것은 호환이 가능하지만, MoveKnight()에 player의 주소를 넘겨주는 것은 호환되지 않습니다. 이도 당연한 것이 "Player는 Knight다." 라는 문장의 의미는 언제나 맞지는 않습니다. 모든 Player가 Knight는 아니기 때문입니다. 반대로 "Knight는 Player다."라는 문장의 의미는 언제나 맞습니다. 즉, 이러한 관계를 통해 다음과 같이 상속받은 여러 class를 최상위 class 매개변수로 뭉퉁그려서 한꺼번에 관리할 수 있는 큰 장점이 있습니다.
// 모든것을 최상위 class로 뭉퉁그려서 상속받는 애들까지 한꺼번에 맞춰 관리할 수 있다.
void MovePlayer(Player *Player){
Player->Move();
}
int main(){
Player p;
MovePlayer(&p);
Knight k;
MovePlayer(&k); // 반대로 player에 knight 넘겨주는 것은 호환이 된다.
return 0;
}
===== 실행 결과 =====
Player Constructer
Move Player
Player Constructer
Knight Constructer
Move Player
하지만 여기서 문제가 하나 있습니다. MovePlayer()에 knight의 주소를 넘겨주었지만 우리가 knight 클래스에서 오버라이딩한 Move()함수가 호출되지 않았고, 부모 클래스의 Move()함수가 호출됨을 알 수 있습니다. 이러한 이유는 일반함수들은 기본적으로 정적 바인딩을 사용하기 때문입니다.
void MovePlayer(Player *Player){
Player->Move();
}
실제로 MovePlayer()에 knight를 넘겨주긴 했지만 함수입장에서만 생각하면 받은거랑은 상관없이 Player -> Move()를 호출하는 것입니다. 이미 정적바인딩으로 딱 묶여있기 때문입니다.
동적 바인딩
이러한 정적바인딩의 문제점을 해결하기위해 동적바인딩을 사용합니다. 동적바인딩을 사용하기위해서는 가상함수(virtual function)를 사용합니다.
virtual void VMove() { cout << "Move Player" << endl; }
이제 MovePlayer()함수를 다음과 같이 변경하겠습니다.
void MovePlayer(Player *Player)
{
Player->VMove();
}
이제부터는 재정의를 하는 과정에서부터 조금씩 달라집니다.
#include <iostream>
using namespace std;
class Player
{
public:
int _hp;
public:
Player()
{
_hp = 100;
cout << "Player Constructer" << endl;
}
void Move() { cout << "Move Player" << endl; }
virtual void VMove() { cout << "Move Player" << endl; } // 가상함수
};
class Knight : public Player
{
public:
int _stamina;
public:
Knight()
{
_hp = 100;
cout << "Knight Constructer" << endl;
}
void Move() { cout << "Move Knight" << endl; }
virtual void VMove() { cout << "Move Knight" << endl; }
};
void MovePlayer(Player *Player)
{
Player->VMove();
}
int main()
{
Player p;
MovePlayer(&p);
Knight k;
MovePlayer(&k);
return 0;
}
===== 실행 결과 =====
Player Constructer
Move Player
Player Constructer
Knight Constructer
Move Knight
이로써 가상함수를 이용해 knight 타입에 맞는 함수가 잘 출력된 것을 볼 수 있습니다. 하지만 실제 객체가 어떤 타입인지 어떻게 알고 알아서 가상함수를 호출해준 것일까요? 이는 가상함수테이블이라는것이 만들어지는데, 이 테이블에서는 호출해야되는 가상함수들의 주소를 들고있는데 해당 객체의 타입(Player,Knight)에따라 값이 바뀝니다.
순수가상함수
순수가상함수란 구현은 없고, 인터페이스만 전달하는 용도로 사용하고 싶을 경우 사용합니다. 다만 상속받는 모든 클래스에서 VAttack()함수를 구현해주어야 합니다.
virtual void VAttack() = 0; // 순수가상함수
추상클래스
순수가상함수가 1개 이상 포함되면 추상클래스로 분류됩니다. 추상클래스로 만든 애들은 직접적으로 객체를 만들 수 없게 됩니다. 즉, 이제부터 Player는 독립적인 객체로 생성될 수 없고, 상속으로만 쓰일 수 있습니다.
'Language > C++' 카테고리의 다른 글
[C++] OOP(객체지향)의 특성 - 연산자 오버로딩 (0) | 2022.06.06 |
---|---|
[C++] OOP(객체지향)의 특성 - 멤버변수 초기화 (0) | 2022.06.06 |
[C++] OOP(객체지향)의 특성 - 캡슐화(encapsulation) (0) | 2022.06.06 |
[C++] OOP(객체지향)의 특성 - 상속성(inheritance) (0) | 2022.06.06 |
[C++] OOP(객체지향)의 개념 (0) | 2022.06.06 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!