Programming Language/C++

[C++] 상속에서의 형변환 - 다운캐스팅

lumana 2024. 6. 27. 23:51

상속에서의 형변환 - 다운캐스팅(Downcasting)

  • 다운 캐스팅 : 부모 클래스에서 자식 클래스로 내려 보내는 형태의 캐스팅

    • static_cast와 dynamic_cast가 있다.
  • static : 정적인 시간(컴파일 타임)에 발생하는 캐스팅

    • ex) double->int, int->double
  • 다음과 같은 클래스가 있다고 해보자

#include <iostream>

using namespace std;

class Base {
   public:
    int a = 1;
};

class Drv1 : public Base {
   public:
    void f() {
        cout << "Drv::f()" << endl;
        cout << b << endl;
    }
    float b = 3.14;
};


int main() {
        Base *b = new Drv1;
        delete b;
        // Drv *d1 = b는 불가능
}
  • b에서는 멤버 변수 a만 접근이 가능하다

  • b에서도 자식 클래스의 고유 멤버에 접근을 해보고 싶다

    • 그러면 b가 가리키는 객체를 Drv *d1 = b로 넣을 수 있지 않을까?

      • 오류가 발생한다. 자식 클래스의 Base *b = new Drv1;는 자동으로 업캐스팅이 되기 때문에 가능하지만, 다운 캐스팅은 자동으로 이뤄지도록 허용하지 않는다. 명시적으로 처리해야 한다.
int main() {
        Base *b = new Drv1;
        Drv1* d1 = (Drv1*)b;
        d1->f();
        delete b;
}
  • Drv1의 f()가 정상적으로 실행된다

  • 다음과 같은 경우를 생각해보자

int main() {
        Base *b = new Drv1;
        int *a = new int(5)
        Drv1* d1 = (Drv1*)a;
        d1->f();
        delete b;
}
  • b를 형변환해야 하는데, 실수로 a를 형변환해버렸다.

    • 이렇게 해도 컴파일 과정에서 오류는 발생하지 않는다. (어차피 형변환해도 주소는 같으니까)

      • 런타임 에러는 발생할 수 있음
  • 이러한 형변환이 상식적인지 아닌지 검사를 해줬으면 좋겠다.

    • 그래서 등장하는게 static_cast이다.

    • 너무 말이 안되는 형변환을 방지해준다. 전혀 다른 타입의 포인터끼리 형변환을 하는 경우 등을 방지해줌

  • Drv *d1 = static_cast<Drv1*>(a); 를 작성하면 에러가 발생함

  • static_cast는 업캐스팅, 다운캐스팅, int->double, double->int 과 같은 형변환을 지원한다

int main() {
        Base *b = new Drv1;
        int *a = new int(5)
        Drv *d1 = static_cast<Drv1*>(b);
        d1->f();
        delete b;
}
  • static_cast의 본질적인 문제점이 한 가지 더 있다.

  • 자식 클래스 하나를 더 만들어 보자

class Drv2 : public Base {
   public:
    void f() {
        cout << "Drv2::f()" << endl;
        cout << c << endl;
    }
    int c = 3;
};

int main() {
    Base *b = new Drv2;
    Drv2 *d2 = static_cast<Drv2*>(b);
    d2->f();
    delete b;
}
  • 3이 출력된다

  • 만약 실수로 b가 Drv2가 아닌 Drv1을 가리키게 된다면?

int main() {
    Base *b = new Drv1;
    Drv2 *d2 = static_cast<Drv2*>(b);
    d2->f();
    delete b;
}
  • 컴파일러는 b가 어떤 객체의 타입(Base, Drv1, Drv2)을 가리키고 있는지는 신경쓰지 않는다

    • static_cast<Drv2*>(b);에서 컴파일러는static_cast를 Drv1*으로 하든, Drv2*로 하든 신경쓰지 않는다. Drv1과 Drv2가 b 타입(Base)의 자식인지만 신경쓴다
  • 위 코드에서는 실제로 가리키는 객체는 Drv1이지만, Drv2로 형변환을 해주고 있다.

  • 실제로 실행해보면, 실행은 되지만 이상한 값을 출력한다

    • 1078523331가 출력된다
  • Base* b는 주솟값을 담고 있다.(Drv1 객체가 저장된 공간을 가리킨다)

  • Drv *d2 = static_cast<Drv2*>(b);에서

    • d2라는 포인터가 생기고

      • b처럼 똑같은 100번지를 담게 된다

      • 그리고 Drv1 타입이지만 Drv2 타입이라고 착각을 하게 된다

  • Drv1가 저장된 메모리의 첫 4바이트에는 a(1)가 저장되어 있고, 그 다음 4바이트에는 b(3.14)가 저장되어 있다.

  • d2->f()를 호출하게 되면, 어떤 동작이 이루어질까?

    • 우리가 예전에 this 포인터를 배우면서, 멤버 함수는 객체 안의 공간에 저장되는 것은 아니라고 배웠다.

    • 멤버함수를 실행하면 어떤 객체의 멤버함수를 실행해야 하는지를 this 포인터를 통해서 전달된다고 배웠다.

    • 그래서 d2->f()에서 멤버 함수의 this 포인터에 d2에 저장되어있는 100번지가 전달되고, c를 출력하는 것은 this->c를 출력하는 것이다.

    • 만약 d2가 Drv2 인스턴스를 정상적으로 가리켰다면, 첫 4바이트(a) 다음 4바이트에 저장되어있는 c의 값을 출력하게 될 것이다.

    • 하지만 지금 d2가 Drv1 인스턴스를 가리키고 있으므로, 첫 4바이트(a) 다음 4바이트에 저장되어있는 b의 값을 출력하게 될 것이다.

      • 3.14는 정규화를 통해 binary로 저장되어있다.
    • Drv2의 두번째 4바이트에는 int로 저장되어 있으므로, float 포맷으로 저장되어 있는 값을, int로 읽은 값이 출력되는 것이다.

      • 3.14가 float 포맷으로 저장된 binary 값을, int로 해석해서 읽은 1078523331가 출력된다.
  • 그래서 static_cast를 사용할 때 타입을 잘 고려해야 한다

    • 코드가 복잡해져서, 프로그래머가 Base *b에서 b가 가리키는 타입이 무엇인지 알기 어려운 경우가 발생할 수 있다.

    • 객체가 어떤 타입인지 런타임에 알 수 있으면 좋지 않을까? --> Dynamic_cast

      • 다음 게시글에서 다룰 예정

참조) 두들낙서 C/C++ 강좌