[OOP] 업 캐스팅, 다운 캐스팅

세동네·2022년 6월 20일
0
post-thumbnail

객체지향에서 캐스팅이란 형변환을 의미한다. 이때 업 캐스팅, 다운 캐스팅은 상속 관계에 있는 객체 간에 일어난다. 상속 관계의 클래스들은 다형성에 의하여 서로의 객체 포인터가 다른 타입의 객체를 가리킬 수 있다.

· 업 캐스팅

업 캐스팅(Up Casting)은 상위 클래스의 포인터가 하위 클래스 객체 주소를 가리키는 것이다. 코드로 표현하면 다음과 같다.

class Base {
public:
    Base() { }

    void print_base() {
        std::cout << "Base\n";
    }
};

class Derived : public Base {
public:
    Derived() { }

    void print_derived() {
        std::cout << "Derived\n";
    }
};

int main() {
    Derived derived;
    Derived* pDerived = &derived;

    Base* pBase = pDerived;
}

원래 알고 있는대로라면 Base * 자료형에는 Base 타입 객체를 동적할당해야 하지만, 파생 클래스인 Derived 객체의 포인터 타입을 저장하였다. 하지만 이는 정상적인 코드이고, 문제 없이 동작한다.

하지만 문제가 발생할 수 있다. 기초 클래스가 파생 클래스 객체를 가리키고 있다고 파생 클래스를 그 자체로 사용할 수 있는 것은 아니다. 당연하게도 컴파일러는 pBase 객체가 Base의 포인터라고 알고 있기 때문에 Base의 멤버만 접근하게 된다.

해당 객체는 Base * 타입으로 정적 바인딩 되었기 때문에 해당 타입을 따라간다.

· 가상 함수

그렇다면 이러한 캐스팅 방식이 아무런 의미가 없는 것 아닌가 싶겠지만, 파생 클래스의 멤버를 사용할 수 있는 방법이 있다. 바로 '가상함수'를 이용하는 것이다. 가상 함수는 '파생 클래스에서 재정의할 것으로 기대하고 정의하는 함수'이다.

class Base {
public:
    Base() { }

    virtual void print() {	// 부모 클래스의 메서드를 가상 함수로 만듦
        std::cout << "Base\n";
    }
};

class Derived : public Base {
public:
    Derived() { }

    void print() override {	// 가상 함수 오버라이딩
        std::cout << "Derived\n";
    }
};

int main() {
    Derived derived;
    Derived* pDerived = &derived;
    
    Base* pBase = pDerived;
    pBase->print();
}

기초 클래스에서 가상 함수로 만들 메서드 선언부 앞에 virtual 키워드를 삽입해주면 가상 함수가 된다. 이후 파생 클래스에서 { 반환형, 식별자, 매개변수 }를 같게 설정하여 함수를 선언하고 식별자 뒤에 override 키워드를 붙이면 가상 함수를 오버라이딩할 수 있다.

- override 키워드의 필요성

  • 파생 클래스의 가상 함수 오버라이딩에서 override 키워드를 붙여주지 않아도 가상 함수 오버라이딩은 정상적으로 이루어진다. 그럼에도 불구하고 해당 키워드를 사용하는 이유는 오류를 줄이기 위함이다.

    만약 위 예시에서 실수로 기초 클래스에서 가상 함수로 만들 메서드에 virtual 키워드를 빼고 작성하였다고 해보자.

    이때 컴파일러는 파생 클래스에서 재정의할 가상 함수가 기초 클래스에서 존재하지 않음을 알리고 오류를 발생시킨다. 또한 오버라이딩이기 때문에 반환형이나 매개변수 정보가 일치하지 않을 때도 오류를 발생시킨다.

    override 키워드를 사용하지 않았다면 해당 오류를 즉시 알지 못했을 것이고, 프로그램을 실행시키면서 뒤늦게 문제를 발견했을 수 있다. 빠른 문제 식별을 위해 사용하는 습관을 들이는 것이 좋다.

· 가상 함수 테이블, 포인터

기초 클래스에서 가상 함수를 선언하면 클래스에 포함된 가상 함수를 관리하는 가상 함수 테이블 vftable을 생성하고, 가상 함수 테이블의 주소를 가리키는 가상 함수 테이블 포인터 vfptr를 객체에 할당한다.

그리고 메서드가 호출되었을 때 가상 함수라면 vfptr로 가상 함수 테이블을 찾아가 호출할 가상 함수의 주소를 추적한다.

이때, 기초 클래스를 상속 받는 파생 클래스도 똑같이 가상 함수 테이블과 포인터를 가진다. 이때 파생 클래스가 가상 함수를 오버라이딩하였다면 자신이 오버라이딩한 함수가 테이블에 저장된다.

각 객체는 고유한 가상 함수 테이블을 가지고(pUpCastingpDerived의 주소를 가리키고 있으므로 테이블의 주소가 같다), 파생 클래스는 자신이 오버라이딩한 가상 함수에 한하여 테이블 정보를 업데이트한다.

프로그램은 이러한 메서드를 호출할 때 객체의 vftable 안에서 호출한 식별자가 어떤 함수 주소를 저장하고 있는지 확인하고 그 함수를 찾아가 실행하는 것이다.

· 다운 캐스팅

업 캐스팅된 즉, 상위 클래스 객체로 캐스팅된 객체를 다시 원래 형태로 되돌리는 캐스팅 방식이다. 앞선 예제 코드에 예시를 위한 함수를 추가하여 다시 작성하였다.

#include <iostream>

class Base {
public:
    Base() { }

    void base_func() {}		// +
    virtual void print() {
        std::cout << "Base\n";
    }
};

class Derived : public Base {
public:
    Derived() { }

    void derived_func() {}	// +
    void print() {
        std::cout << "Derived\n";
    }
};

int main() {
    Derived derived;
    Derived* pDerived = &derived;
    
    Base* pBase = pDerived;
    pBase->print();
    pBase->derived_func();

    Derived* down_object = (Derived*)pBase;
    down_object->derived_func();
}

기초 클래스, 파생 클래스에 각각 base_func(), derived_func()함수가 추가되었다. 앞서 말한 것처럼 pBase 객체는 Derived 클래스의 멤버를 사용할 수 없다. 즉,

이처럼 pBase 객체가 derived_func() 함수를 호출하려 하면 에러를 일으킨다. 이때 pBase 객체에 Derived * 타입으로의 명시적 형변환을 달고 Derived *로 강제 형변환을 해줄 수 있는데, 이것이 다운 캐스팅이다.

Derived* down_object = (Derived*)pBase;

이렇게 되면 pBase에서 접근할 수 없었던 Derived 클래스의 멤버에 접근할 수 있게 된다.


· 참고

C++ 업캐스팅 다운캐스팅
C++ 가상 함수(virtual, override 키워드)
가상함수테이블 C++

0개의 댓글