카테고리 없음

c++ 다형성

solie75 2022. 6. 23. 15:41

다형성

다형성이란 상위 클래스의 포인터나 레퍼런스가 하위 클래스 객체를 가리킬 때 파생 클래스에서의 가상함수 재정의에 의해 호출되는 함수가 달라지는 것을 말한다.

 

다음과 같은 상속 구조의 클래스가 있다고 가정해 보자.

class Weapon
{
public:
    void Attack()
    {
        std::cout << "Attact the enemy" << std::endl;
    }

    Weapon() {}
    ~Weapon() {}
};

class Sword : public Weapon
{
public:
    void Attack()
    {
        std::cout << "Stab an enemy" << std::endl;
    }

    Sword() {}
    ~Sword() {}
};

class Bow : public Weapon
{
public:
    void Attack()
    {
        std::cout << "Shoot an Arrow" << std::endl;
    }

    Bow() {}
    ~Bow() {}
};

Knife 와 Bow 클래스는 각각 Attact 함수를 재정의 한 상태이다.

int main()
{
    Sword sword1;
    Bow bow1;

    sword1.Attack();  // ...ㄱ
    bow1.Attack();  // ... ㄴ

    Weapon* weaponPtr = nullptr;

    weaponPtr = &sword1;
    weaponPtr->Attack();  // ... ㄷ

    weaponPtr = &bow1;
    weaponPtr->Attack();  // ... ㄹ

    return 0;
}

ㄱ은 Sword 클래스의  Attact 함수를, ㄴ은 Bow클래스의 Attact 함수를 호출한다.

하지만  ㄷ과 ㄹ은 Weapon 클래스의 Attact 함수를 호출한다.

c++ 에서는 참조하는 대상의 타입이 아닌 맴버를 호출하는 변수(포인터 혹은 객체 변수)의 타입에 따라 그 타입에 맞는 맴버를 호출하기 때문이다.

다시말해 ㄷ에서 weaponPtr 은 Sword 타입 객체의 주소를 담고 있지만 weaponPtr 자체는 Weapon 타입이기 때문에 C++ 의 포인터 변수, 호출하는 변수의 타입을 더 중요시한다는 특성을 따라 Weapon 클래스의  Attact() 함수를 호출하는 것이다.

위의 두 코드의 결과 값은 다음과 같다.

이와 같이 일일이 자식 타입의 변수로 맴버 함수를 호출해주어야 하는 번거로움을 다향성을 통해 해결할 수 있다. 하지만 부모 포인터로 자식 클래스의 객체들의 주소를 가리켜도 실제 사용 가능한 기능은 부모 클래스의 맴버에 그친다는 문제점이 있다.

 

Virtual Function (가상함수)

다향성의 문제점을 해결하기 위해 부모 타입 포인터로 자식 객체를 가리킬 시 자식이 오버라이딩 한 것을 호출 가능하도록 오버라이딩 된 부모 맴버 함수에 대해 virtural (가상 함수)지정을 하면 된다.

class Weapon
{
public:
    virtual void Attack() // ...ㄱ
    {
        std::cout << "Attact the enemy" << std::endl;
    }

    Weapon() {}
    ~Weapon() {}
};

class Sword : public Weapon
{
public:
    virtual void Attack() override  // ... ㄴ
    {
        std::cout << "Stab an enemy" << std::endl;
    }

    Sword() {}
    ~Sword() {}
};

ㄱ 과 같이 맴버 함수의 맨 앞에 virtual 을 붙여주면 가상 함수로 선언된다.

가상함수는 동적바인딩으로 실행 시에 메모리를 할당( = 구속(바인드))한다 >> 가상함수, 변수 동적 할당

( 반면에 일반 변수, 일반 변수 할당의 경우 정적바인딩(컴파일시에 메모리를 할당(=구속(바인드))을한다 

이는 부모 타입의 포인터로 호출하더라도 자식 클래스가 오버라이딩한 함수를 호출하게 끔 한다.

자식 클래스에서는 가상함수를 오버라이딩 하기를 권장한다(가상함수가 시작점인지 아닌지를 구별하기 위해 ㄴ과 같이 뒤에 override 를 붙인다.)

 

가상함수 포인터 (vfptr) 은 가상함수를 추가할 경우 객체의 맴버 변수로 가상함수 테이블을 가리키는 포인터를 해당 가상 함수의 맴버 변수로 추가를 시킨다.

#include <iostream>

class Weapon
{
public:
    virtual void Attack()
    {
        std::cout << "Attact the enemy" << std::endl;
    }

    Weapon() {}
    ~Weapon() {}
};

class Sword : public Weapon
{
public:
    virtual void Attack() override
    {
        std::cout << "Stab an enemy" << std::endl;
    }

    Sword() {}
    ~Sword() {}
};

class Bow : public Weapon
{
public:
    /*virtual void Attack() override
    {
        std::cout << "Shoot an Arrow" << std::endl;
    }*/

    Bow() {}
    ~Bow() {}
};

int main()
{
    Weapon weapon1;
    Sword sword1;
    Bow bow1;
    

    weapon1.Attack();
    sword1.Attack();
    bow1.Attack();

    Weapon* weaponPtr = nullptr;

    weaponPtr = &sword1;
    weaponPtr->Attack();

    weaponPtr = &bow1;
    weaponPtr->Attack();

    return 0;
}

Bow클래와 Sword 클래스 모두 재정의 하지 않은 경우

 

위의 코드에와 같이 Sword 클래스는 재정의를 하고 Bow 클래스는 재정의를 하지 않았을 때

 

재정의 를 했을 때 sword 클래스를 자료형으로 하는 객체 sword1의 Attack 함수의 주소가 바뀐것을 확인 할 수 있다.

 

가상함수 테이블

각 클래스는 컴파일러에 의해 클래스 정보가 생성되고 여기에는 가상함수 테이블도 존재한다.

간단히 'V-table(virtual table)이라고도 하는데, 이는 실제 호출되어야 할 함수의 위치 정보를 담고 있다.

가상 함수 테이블을 가상함수가 있는 클래스 당 1개만 만들어진다.

#include <iostream>

class Sword
{
private:
	int swordNum = 2;

public:
    virtual void Cut()
    {
        std::cout << "Cut the enemy with sword" << std::endl;
    }

    virtual void pierce()
    {
        std::cout << "pierce an enemy" << std::endl;
    }

    virtual void Stab()
    {
        std::cout << "Stab the enemy" << std::endl;
    }
    
    void defend()
    {
        std::cout << "Defend the attack" << std::endl;
    }


    Sword() {}
    ~Sword() {}
};

class Dagger : public Sword
{
private:
	int daggerNum = 1;

public:
    virtual void Cut() override
    {
        std::cout << "Cut the enemy with dagger" << std::endl;
    }

    virtual void Throw()
    {
        std::cout << "Throw against the enemy" << std::endl;
    }

    Dagger()
    {}

    ~Dagger()
    {}
};


int main()
{
    Sword sword1;  // ... ㄱ
    Dagger dagger1;  // ... ㄴ
    
    std::cout << sizeof(dagger1) << std::endl; // ...ㅁ

    Sword* swordPtr = new Sword;  // ... ㄷ
    Sword* daggerPtr = new Dagger;  // ... ㄹ

    return 0;
}

 

 

한 개 이상의 가상함수를 포함하는 클래스에 대해서 컴파일러는 다음 표와 같은 형태의 '가상함수 테이블'을 만든다.

ㄱ 의 경우

Key Value
vtable 주소 0x00007ff61346bf68
vtable 소멸자 ?? ( 디버깅 로컬에 표시 되지 않음...왜그럴까요..?)
void Sword::Cut() 0x00007ff67b3d117c
void Sword::Pierce() 0x00007ff67b3d12df
void Sword::Stab() 0x00007ff6134615b4

- key는 호출하고자 하는 함수를 구분지어주는 구분자의 역할을 한다.

- value 는 구분자에 해당하는 함수의 주소 정보를 알려주는 역할을 한다.

 

sword1 객체의 Cut() 함수를 호출해야 할 경우 위의 테이블의 첫 번째 행의 정보를 참조하여 0x00007ff67b3d117c에 등록되어 있는 Cut() 함수를 호출하게 되는 것이다.

 

Dagger클래스 또한 내에 가상함수가 존재하므로 다음 표와 같이 가상함수 테이블이 생성된다.

ㄴ의 경우

Key Value
vtable 주소 0x00007ff61346bf68
vtable 소멸자 ??
void Dagger::Cut() 0x00007ff61346157d
void Sword::Pierce() 0x00007ff613461587
void Sword::Stab() 0x00007ff6134615b4
void Dagger::Throw() ??(왜 이것도 디버깅 로컬에 안나타나는 걸까요...하루종일 삽질해도 모르겠어요 분명 Dagger::`vftable'[5])()를 보면 주소 소멸자 맴버함수 다 포함해서 6행이 나와야 하는데...ㅜㅠ)

자식 클래스의 vtable은 부모 클래스의 vtable 값이 그대로 복사되며, 오버라이딩 된 함수만 주소가 새로 없데이트 된다고 한다. 그리고 만약 자식 클래스에 부모에 없는 새로운 가상함수를 추가할 경우, 객체의 vtable 마지막 부분에 추가된다.

 

객체를 동적할당 할 때 가상 함수 테이블에 들어가는 예시

ㄷ, ㄹ 디버깅 로컬

ㄷ, swordPtr 은 본인 객체를 생성하고 있고

ㄹ, daggerPtr 은 자식 클래스인 Dagger 을 생성하고 있다.

 

vfptr 이 3개인 이유는 daggerPtr 이 생성되는 객체가 자식이기 때문에 부모클래스인 Sword 먼저  생성하고 다음 본인(자식)클래스가 생성되기 때문이다.

 

daggerPtr 이 할당될 때 자식 클래스에 재정의된 함수가 있는지 확인하고, 재정의가 안된 함수라면 그대로 두고, 재정의 된 함수가 있으면 가상함수 테이블에 해당 함수 주소를 재정의된 함수의 주소로 다시 바인드 한다(맨 위에 _vfptr). 그다음에 자식에게 상속해준다(중간 _vfptr).

 

*동적 바인딩(virtual/ 프로그램 실행 중)

1. 가상함수가 아니라면 -> 이미 할당된 함수를 가지고 온다

2. 가상함수라면   -> 재정의된 함수가 없을 경우 : 부모클래스 가상함수 그대로

                             -> 재정의된 함수가 있을 경우 : 재정의된 함수

 

* ㅁ의 결과 는 24이다. 객체 dagger1의 메모리 구조를 살펴보면

((부모)Sword 클래스) ((자식)Dagger 클래스)
8Byte 4Byte 4Byte 4Byte 4Byte
_vfptr swordNum padding daggerNum padding

위와 같다. 가장 부모 클래스를 맨 앞에 두고 순서대로 나열하면 된다.