카테고리 없음

Dynamic_casting

solie75 2022. 6. 24. 12:02

업 캐스팅, 다운 캐스팅

int a = 1;
int* aPtr = &a;

와 같이 ~형 포인터는 ~형의 주소값을 받는다. int 형 포인터는 int 형 변수의 주소값을 받아야한다.

클래스도 마찬가지 이다.

class A
{
public:
    int a;
};

class B
{
public:
    int b;
};


int main()
{

    A a;
    B b;
    A* aPtr = &a;
    A* bPtr = &b;  // ... ㄱ
    
    return 0;
}

다음과 같은 상황에서 ㄱ은 

위의 사진과 같은 오류가 생긴다.

 

하지만 그렇다고 각 클래스에 각자가 필요한 모든 정보를 다 두려고 하면 메모리가 쓸데 없이 너무 커지게 된다. 따라서 공통된 항목들을 부모 클래스로 정의되는 타 클래스로 묶어 두기로 하였다. 그리고 공통된 정보를 부모 클래스의 반대인 자식클래스가 사용하려할 때 사용되는 개념이 바로 상속이었다. 위의 코드를 부모 클래스와 자식클래스로 구별하면

class A
{
public:
    int a;
};

class B : public A  // ... ㄴ
{
public:
    int b;
};


int main()
{

    A a;
    B b;
    A* aPtr1 = &a;
    A* bPtr1 = &b;  // ... ㄱ
    
    A* bPtr2 = new B();  // ... ㄴ
    
    A* aPtr2 = nullptr;  // ... ㄷ(1)
    B* bPtr3 = new B();  // ... ㄷ(2)  
    aptr2 = (A*)bPtr3;  // ... ㄷ(3)
    
    
    return 0;
}

자식 클래스에 부모 클래스로 삼으려는 클래스를 대상으로 ㄴ과 같이 코드를 짜면 된다.

이제   ㄱ 은 더이상 오류를 일으키지 않는다 B 클래스가 A클래스를 상속 받았기에 객체 b의 주소를  A 포인터인 bPtr 에 대입해도 괜찮은 것이다. 이와 같이 자식 클래스의 객체를 부모 클래스 포인터에 대입하여 클래스를 바꾸는 것만 같은 효과를 내는 것을 '업 캐스팅' 이라 한다.

 

ㄴ과 ㄷ(1~3)에서

B를 A로 바꾸었다기 보다는 

메모리에는 B 고대로 존재하되 (동적할당)

그 시작점을 가리키는 포인터를 부모로 가리키겠다는 의미이다.

다시 말해, 부모 포인터로 자식 객체를 가리키겠다는 의미.

 

이 업 캐스팅이 다형성 즉 상속의 바탕이 된다.

 

이와 반대로 부모 클래스의 객체를 자식 클래스의 포인터에 대입하여 클래스를 바꾸는 것 같은 효과를 내는것을 ' 다운 캐스팅 ' 이라 한다.

다운캐스팅의 필요성을 위해 코드를 다르게 짜보자

class Sword
{
private:
    int swordnum = 2;

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

class Dagger : public Sword
{
private:
    int daggernum;

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

    void PrintDaggerNum()
    {
        std::cout << "Character have " << daggernum <<" daggers" <<std::endl;
    }
    
    Dagger(int _num = 123)
        : daggernum(_num)
    {

    }

    Dagger()
    {}

    ~Dagger()
    {}
};

void Func(Sword* _swordPtr)
{
    _swordPtr->Cut();
    ((Dagger*)_swordPtr)->PrintDaggerNum();
}

int main()
{
    Sword sword1;
    Dagger dagger1(5);

    Func(&dagger1);
    Func(&sword1);

    return 0;
}

 위의 코드에는 두개의 클래스가 정의 되어 있다. Sword는 Cut 이라는 가상 함수만을 가진다. Sword로부터  파생된 Dagger 는 int 형의 맴버 변수와 이 맴버를 초기화 하는 생성자 그리고 PrintDaggerNum이라는 비가상 맴버 함수를 가지며 상속 받은 Cut 가상함수는 다른 문자열을 출력하도록 재정의 되어 있다.

 Func 함수는 Sword 또는 그 파생 객체의 포인터를 인수로전달받아 Cut 가상 함수를 호출한다. 그리고 객체가  Dagger 타입이면 이 객체의 PrintDaggerNum 이라는 비가상 함수도 호출한다.

 Sword 객체 뿐만 아니라 그의 파생 객체인 Dagger 객체 또한 전달받아야하기 때문에 최상위 클래스인 Sword 타입의 포인터를 전달 받을 수밖에 없다.

 PrintDaggerNum 함수는 Dagger 에만 있으므로 이 함수를 호출하려면 Sword* 타입의 인수 _swordPtr 을 Dagger* 타입으로 강제캐스팅 해야 한다. main에서는 각 클래스의 객체  sword1과 dagger1을 선언하되, dagger1의 daggernum은 5로 초기화 한다. Func 에 객체 sword1과 dagger1의 주소값을 각각 전달할때

이와 같은 결과가 나온다

Func(&dagger1) 호출로 Dagger* 형 객체의 주소를 전달한 때에는 Cut과 PrintDaggerNum 두 호출이 모두 성공이다. Cut 은 가상함수이르모 객체의 타입에 맞는 함수가 호출될 것이고 PrintDaggerNum은 비가상 함수이지만 Dagger 함수이므로 

_swordPtr을 Dagger* 타입으로 전달하면 제대로 동작한다.

하지만 Func(&sword1)로 전달할 때 가상함수인 Cut은 vtable 에서 실제 번지를 찾으므로 제대로 작동하지만 비가상 함수인 PrintDaggerNum은 엉뚱하게 동작한다. 인수_swordPtr 이 가리키는 객체는 daggernum 이라는 맴버를 가지고 있지 않은데 이 객체를 강제로(그리고 일시적으로) Dagger*로 캐스팅 했기 때문이다.

 캐스팅을 하였으니 일단 컴파일되지만 이때 PrintDaggerNum이 읽는 daggernum맴버는 sword1 객체에 존재하지 않는다. sword1의 타입인 Swrod 클래스의 dagger num 맴버에 대한 오프셋 위치(this->daggernum)을 무조건 읽는데 이 번지에 사용자가 입력한 값이 있지 않으므로 쓰레기 값이 출력되는 것이다.

위의 코드를 그림으로 보면 다음과 같다.

 Sword 클래스의 vtable 에는 자신의 가상함수 Cut에 대한 정보만 들어있고 Dagger 클래스의 vtable 에는 Cut과 PrintDaggerNum의 번지가 들어있다. 객체 sword1은 생성될 때 Sword 타입으로 생성되었으므로 sword1의 메모리 선두에는 Sword의 vtable을 가리키는 vfptr이 존재한다. 이 vtable에는 PrintDaggerNum이라는 함수의 번지가 들어있지 않다. 그런데 Func 함수에서는 sword1을 Dagger*로 다운캐스팅 하였으므로 컴파일러는 이 번지에 PrintDaggerNum이 있을 것으로 판단하고 에러를 발생시키지 않는다.

 하지만 실행하면 sword1이 가리키는 vtable의 두번째 항복은 함수의 번지가 아닌 쓰레기 값이 들어있고 이 쓰레기 값을 함수 번지 삼아 점프하면 프로그램이 어떻게 작동 할지는 모르는것이다.

원래의 Func함수의 의도가 Sword의 파생 객체를 받아 그 객체로 어떤 작동을 하되 객체가 Dagger 타입인 경우에만 PrintDaggerNum 을 호출하고 싶은것이라면 Func함수는 다음과 같이 작성해야한다.

void Func(Sword* _swordPtr)
{
    _swordPtr->Cut();
    if (_swordPtr이 Dagger의 객체라면)
    {
        ((Dagger*)_swordPtr)->PrintDaggerNum();
    }
}

위의 설명에서 중요한 것은 다운 캐스팅을 위해서는 실행시간에 객체의 타입에 대한 정보를 할 수 있어야하고 두번째로 컴파일에 걸리지 않는 불안요소를 컨트롤 하는것이다.

다운캐스팅의 안정성을 높히기 위해 Dynamic_cast을 사용하는데 이를 위해  c++ 에서는 객체만 가지고는 어떤 클래스의 객체인지 알 수 있는 방법이 원래 없기에 RTTI(Runtime Type Intormation, 실행시간에 객체의 타입에 대한 정보를 얻은 수 있는 기능)

Dynamic_cast는 객체가 어떤 클래스를 자료형으로 두는지 확인하고 틀리다면 nullptr 을 맞다면 다운캐스팅된 주소를 반납한다.

위의 코드의 다운캐스팅 부분을 동적 캐스팅으로 바꾸면

void Func(Sword* _swordPtr)
{
    _swordPtr->Cut();

    Dagger* daggerPtr = dynamic_cast<Dagger*>(_swordPtr);
    if (nullptr != daggerPtr)
    {
        daggerPtr->PrintDaggerNum();
    }
}

이와 같이 된다.