Polymorphism = Poly + morph
겉은 똑같은데, 기능이 다르게 동작한다.
다형성을 제공하는 여러가지 요소들 중 유명한게
오버로딩 : 함수 중복 정의
오버라이딩 : 함수 재정의 (부모 클래스의 함수를 자식에서 재정의)
이런함수가 있다고 가정을 하자.
main에서
MovePlayer(&p);는 가능하지만, MoveKnight(&p)는 안된다.
하지만 MoveKnight(&k);도 되고 MovePlayer(&k);도 가능하다.
주소 값 | 변수명 |
---|---|
[Player 클래스(객체)의 주소] + [뒤에 아무것도 없음] | p (Player의 객체) |
[Player 클래스(객체)의 주소] + [Knight 클래스(객체)의 주소] | k (Knight의 객체) |
자식 클래스 객체는 항상 앞에 부모 클래스의 주소를 앞에 놔둔다.
그래서 MovePlayer의 인자로 k를 넘겨줄 수는 있지만
p를 Knight* 를 받는 MoveKnight의 인자로는 넘겨 줄 수 없다.
void MoveP(Player* player)
{
cout << "Movep" << endl;
}
void MoveK(Knight* knight)
{
cout << "Movek" << endl;
}
int main()
{
Player p;
Knight k;
MoveP(&p);
MoveK(&p); // 이부분이 에러임.
MoveP(&k);
MoveK(&k);
return 0;
}
void Move(Player* player)
{
player->Move();
}
Knight k;
MovePlayer(&k);
이렇게 넘겨주게 되면
Knight의 Move를 호출 할까? Player의 Move를 호출 할까?
정답은 Player의 Move함수를 호출한다.
아까 말한 것처럼
주소 값 | 변수명 |
---|---|
[Player 클래스(객체)의 주소] + [뒤에 아무것도 없음] | p (Player의 객체) |
[Player 클래스(객체)의 주소] + [Knight 클래스(객체)의 주소] | k (Knight의 객체) |
k를 넘겨주게 되면
" [Player 클래스(객체)의 주소] + [Knight 클래스(객체)의 주소] "
이녀석이 넘어가기 때문에
컴파일러가 인자로 넘어온 첫번째 주소를 받아서
Player->Move를 호출한다.
이러한 현상을 유식한 말로
이라고한다. == 묶는다.
정적 바인딩(Static Binding) : 컴파일 시점에 결정
동적 바인딩(Dynamic Binding) : 실행 시점에 결정 ❗(면접 단골)
일단, 바인딩이라고 하면은
프로그램 실행하면 cpp언어로 작성한 코드를 컴파일러가
기계어로 열심히 열심히 번역 할 것이다.
그중에서
이런 함수를 호출 하는 부분에서 어떤 함수를 호출 해야하는지를
매핑해주게 되는데 -> 연결해주게 되는데,
그부분을 "바인딩"이라고 볼 수 있다.
일반 함수들은 이 "정적 바인딩"을 사용한다.
이 MovePlayer라는 함수만 놓고 보면은
매개변수의 값이 Player인지 Knight인지는 알 수 없고
매개변수로 인자가 들어왔고 Move를 호출 했을 뿐이다.
그래서 그냥 Player클래스의 포인터를 받아와서
player->Move();를 호출 했다.
=> 즉, 이렇게 하기로 묶여 있다는 것이다.
"정적으로 바인딩 되어있다"는 것이다.
이게 컴파일 단계에서 이미 묶여 있는 것이다.
동적바인딩을 사용하기를 원한다면
"virtual function"을 사용해야한다.
먼저,
Player에서 virtual로 함수를 만들어 놓자.
그리고 Knight에서
이렇게 가상 함수를 만드는데
한가지 알아야 할게
가상함수는 재정의를 하더라도 가상 함수이다!
최상위 부모가 함수에 virtual을 붙여놓은 순간,
그 다음에
void VMove() 로 virtual 키워드 지워도 virtual붙인거랑 똑같이 동작을 한다.
그래서 이렇게 만들어 주고 다시,
Knight k;
MovePlayer(&k);를 넘겨주어서
이 함수를 호출을 하면은
Knight의 Move함수가 호출이 된다!
왜❓❓❓
=>
아까 정적 바인딩에서
매개변수의 값이 Player인지 Knight인지는 알 수 없고
매개변수로 인자가 들어왔고 Move를 호출 했을 뿐이다.
라고했는데
이것에 대해서 알아볼 것이다.
virtual 키워드를 붙인 함수를 부모 && 자식에 추가한 경우와
안 추가한 경우의 클래스 객체의 메모리를 확인 해보도록 하자.
class Player
{
public :
int m_iHp = 10;
int m_iAttack = 20;
int m_iDefense = 30;
void MovePlayer()
{
cout << "Move Player" << endl;
}
/*virtual void PrintPlayer()
{
cout << "Print Player" << endl;
}*/
};
class Knight : public Player
{
public:
int m_iStat = 10;
void MoveKnight()
{
cout << "Move Knight" << endl;
}
/*virtual void PrintKnight()
{
cout << "Print Knight" << endl;
}*/
};
int main()
{
Player p;
Knight k;
return 0;
}
&k의 주소를 보면은
이렇게 부모 클래스의 멤버 변수부터 메모리가 잡힌 상태로
4바이트 * 4 = 16바이트가 잡혀있는거 확인 가능하다.
virtual을 붙이고 다시 메모리를 들어가보면
아까와는 다르다는 것을 알 수 이상한 메모리 값들이 추가가됨.
알 수 있는 사실이 "가상 함수"가 추가가 되는 순간 메모리에 뭔가가 추가가 된다는 것이다.
저 이물질 같이 끼인 녀석이 "가상함수"를 찾는데
중요한 역할을 하는 녀석이다.
그래서 MovePlayer(Player* player) 함수는
실제객체가 어떤 타입인지 어떻게 알고 알아서 가상함수를 호출 하느냐?
=> 가상 함수 테이블 (vftavle) 때문에 가능하다!
이 테이블은 무엇이고 뭘 들고있는 것이냐?
virtual 테이블도 일종의 주소인데 (포인터 처럼)
주소이다 보니까
32비트 프로그램에서는 4byte, 64비트 프로그램에서는 8byte의
고정 크기를 가진다.
그래서 포인터 처럼 .vftable을 타고 들어가면은
[][] [][] []
이런 바구니 들이 있을 텐데
.vftable
[주소] , [주소] , [주소] , [주소] , [주소]
이런식으로 주소가 들어가있다.
virtual VMove라는 함수를 하나만 만들어놓은 상태에서는
.vftable
[VMove] ,
이런식으로 들어가있는 상태이다.
이 함수에 Player p를 넘겨주어서 메모리를 보면은
지금 딱 드래그 해놓은 부분이 vftable의 "주소"인 것을 알 수 있다.
이것을 디스어셈블리로 까보면은
player는 포인터인데, 그 주소를 타고 가서 그 주소를
eax에 mov했는데 그 주소가
지금 Player의 객체가 저장된 주소이다. 이거임.
그리고 그 주소의 첫번째 값을 edx에 꺼내온것을 볼 수 있다.
말그대로 이값을 edx 레지스터에 꺼내온 것이다.
CALL을 하는 부분인 이 부분이 핵심인데
edx에 있는 값을 다시 eax에 넘겨준다음에
그거를 다시 call eax를 하고있다.
결국에는 이런식으로 vftable의 첫번째 주소는 이렇게 있고
두번째주소는 만들어준 VDie의 함수의 주소가 있는 것이다.
이 주소는 객체가 Player의 객체인지
Knight의 객체인지에 따라
vftable이 들고있는 주소값이 다르다는 것이다.
Player p의 주소
Knight k의 주소
이렇게 서로 다른 객체의 가상함수테이블 주소도 다르다는 것을 알 수 있다.
즉,
매개변수로 player에 어떤애가 들어올지는 모르겠지만
매개변수로 전달되어서 오는 녀석의 첫번째
주소에는 "표지판"을 들고있는 것이다.
&p의 첫번째 주소값으로 vftable 주소를 이렇게 들고있는 것임.
=> 생성자에서 해주게된다.
이렇게 생성자 부분에서 vftable 채워주는 부분 보인다.
먼저 Player생성자 부분을 채워주게 되고
이렇게 Player의 가상함수 테이블 주소로 채워진 다음에, 그 다다음줄에서
이코드에 의해
원본객체의 함수 주소로 덮어 써주게 된다.
순수 가상 함수 : 구현은 없고 "인터페이스"만 전달하는 용도
모던 C++에서는
일반 C++에서는
이렇게 표현한다.
=> 구현은 하지 않고 Player를 상속받는 애가 구현을 해라! 이말임.
재정의 해야한다는 강제가 주어진다.
"순수 가상 함수"가 1개 이상이라도 있다면 (클래스 안에)
그 클래스는 "추상 클래스"로 간주한다.
추상 클래스는 직접적으로 객체를 만들 수 없게된다.
Player p 하게 되면 에러난다. (객체를 만들 수 없기 때문에)
class Parent
{
void Move() {}
};
class Child : public Parent
{
void Move() {}
};
void MoveFunc(Parent* parent)
{
parent->Move();
}
이런식으로 정의되어 있을 경우
아쉬운 점은 MoveFunc에다가 Parent 객체를 넘겨준 경우에는
Parent 클래스의 Move를 호출을 하지만
Child객체를 넘겨준 경우에도 Parent클래스의 Move를 호출하게 된다.
< 원인 >
< 해결 >
동적 바인딩을 사용한다.
즉,
가상함수를 만든다.