티스토리 뷰

업캐스팅 (Upcasting)

업캐스팅은 자식 클래스 타입에 대한 참조(포인터 혹은 레퍼런스)를 부모 타입으로 형변환하는 것이다.

 

부모클래스 *b = new 자식클래스;

 

위 코드는 분명 다른 자료형임에도 어떠한 오류가 발생하지 않는다. 이는 자식 클래스에서 부모 클래스 객체에 할당될때 묵시적 형변환이 일어나기 때문이다. (실제로 모든 업캐스팅은 묵시적으로 일어날 수 있다.)

 

이는 포인터뿐만 아니라 좌변이 부모 클래스 레퍼런스 변수일 때도 일어난다. 이는 Call-By-Reference에도 발생한다.

 

자식클래스 d;
부모클래스 &p = d;
void f(부모클래스 &b) {
    // ...
}

int main() {
    자식클래스 d;
    f(d);
)

 

하지만 포인터나 레퍼런스를 사용하지 않고 객체 자체끼리 대입하려 하면 업캐스팅이 아니기에 컴파일 오류가 발생한다.

 

자식클래스 d;
부모클래스 b = d; // 에러!! 이건 업캐스팅이 아니다.

 

 

업캐스팅의 잘못된 예제

업캐스팅의 개념은 간단하고 구동하는 방식도 간단하다. 하지만 업캐스팅은 형변환이 발생하는 만큼 메모리 할당 크기가 변하기 때문에 항상 주의해야한다.

 

#include <iostream>
using namespace std;

struct Animal {
    float xpos = 1;
    float ypos = 2;
};

struct FlyingAnimal : public Animal {
    float zpos = 3;
};

void printAnimals(Animal *a, int n) {
    for (int i = 0; i < n; i++) {
        cout << "(" << a[i].xpos << ", " << a[i].ypos << ")" << endl;
    }
}

int main() {
    FlyingAnimal *arr = new FlyingAnimal[5];
    printAnimals(arr, 5);
    delete[] arr;

    return 0;
}
(1, 2)
(3, 1)
(2, 3)
(1, 2)
(3, 1)

 

위 코드는 xpos, ypos, zpos 값이 들어있는 FlyingAnimal 배열을 Animal 클래스로 업캐스팅하여 내부 값을 출력하는 것이다. 간단하게 생각하면 xpos, ypos만 출력하기에 (1, 2)만 출력이 되어야 하지만, 실제로 zpos가 사이사이에 출력이 된다.

 

이는 실제 FlyingAnimal 배열의 각 객체는 xpos, ypos, zpos 3개의 int형 변수가 할당되어 있지만, Animal로 업캐스팅 되면부모의 자료형이 되어버린 객체들은 zpos변수에 대한 정보가 없기 때문에 int형 변수 2개 만큼의 메모리 할당 영역을 참조할 수 있기 때문에 이러한 현상이 발생한다.

 

 

이러한 현상은 배열 각 원소들이 다형성이 실현되지 않았기에 발생한다. 이에 더블 포인터를 사용하면 해당 현상을 예방할 수 있다.

 

 

해결방법

#include <iostream>
using namespace std;

struct Animal {
    float xpos = 1;
    float ypos = 2;
    virtual ~Animal() {} // 가상 소멸자
};

struct FlyingAnimal : public Animal {
    float zpos = 3;
};

void printAnimals(Animal **a, int n) {
    for (int i = 0; i < n; i++) {
        cout << "(" << a[i]->xpos << ", " << a[i]->ypos << ")" << endl;
    }
}

int main() {
    Animal **a = new Animal *[5];
    for (int i = 0; i < 5; i++) {
        a[i] = new FlyingAnimal;
    }

    printAnimals(a, 5);

    for (int i = 0; i < 5; i++) {
        delete a[i];
    }

    delete[] a;

    return 0;
}
(1, 2)
(1, 2)
(1, 2)
(1, 2)
(1, 2)

 

위 코드는 더블 포인터를 통해 Animal 배열을 생성하고 각 Animal 배열 원소에 자식 클래스를 할당하여 각 객체가 연속적은 메모리를 할당하지만 객체마다 자신만의 고유 상태 존재하게 되어버린다. 하여 각 원소가 3개의 int형 변수의 연속된 집합이 아닌 클래스 객체가 되어준다. 각 객체는 힙영역에 서로 다른 주소에 할당되어 있기에 상관이 없고 더블 포인터 배열은 해당 객체를 가리키는 포인터 주소만 가지고 있게 되는 것이다.

 

 

 


다운캐스팅 (Downcast)

다운캐스팅은 부모가 자식으로 형변환하는 것이다. 개념 자체는 간단하지만 업캐스팅은 존재하는 정보를 줄이는 것이지만 다운캐스팅은 존재하지 않는 정보를 생성해야하기 때문에 무조건 성립하지 않는다.

 

업캐스팅
#include <iostream>

using namespace std;

struct Base {
    int a = 1;
    virtual ~Base() {}
};

struct Drv1 : public Base {
    float x = 3.14;
};

struct Drv2 : public Base {
    int y = 3;
};

int main(void) {
    Base *b = new Drv1;
    
    cout << b->x << endl; // 에러!
    
    return 0;
}

 

위와 같은 경우, 부모 구조체 객체 포인터인 b는 자식 클래스 Drv1를 넣은 업캐스팅 객체이다. 하여 부모 객체는 자식의 정보를 알수없어서 x에 대한 접근이 불가능하다. 하지만 부모 객체라도 다운캐스팅이 필요할 수 있다.

 

우선 다운캐스팅은 묵시적으로 일어날 수 없다. 따라서 항상 형변환을 하겠다고 명시해야하며 강제 형변환도 가능하다.

 

다운 캐스팅
#include <iostream>

using namespace std;

struct Base {
    int a = 1;
    virtual ~Base() {}
};

struct Drv1 : public Base {
    float x = 3.14;
};

struct Drv2 : public Base {
    int y = 3;
};

int main(void) {
    Base *b = new Drvl;
    Drv1 *dl = b; // 에러!! 다운캐스팅은 묵시적으로 일어날 수 없다.
    Drvl *dl = (Drvl *)b; // 다운캐스팅(강제 형변환)
    
    return 0;
}

 

 

static_cast

C++은 몇 가지 다양한 형변환 연산자를 제공하며, static_cast는 그 중 하나다. static_cast는 컴파일 타임에 수행되는 안전한 형변환을 수행한다. static_cast와 dynamic_cast같은 형변환 방식은 방향이 아닌 방식 중 하나이기에 모두 업캐스팅과 다운캐스팅이 가능하다.

 

가장 먼저 알아야하는 부분은 컴파일러는 "다운캐스팅이 올바르게 일어났는가?"를 알 수 없다. 즉, 상속 관계가 아닌 두 타입의 변환은 컴파일러가 오류로 처리할 수 있지만, 상속 관계에 서로 다른 자료형은 의도한 방향으로 형변환이 진행되는 것인지 전혀 알 수 없다.

 

  1. 안전성 및 검사
    • static_cast는 컴파일 타임에 타입 검사를 수행하므로, 형변환이 안전한지 여부를 컴파일러가 판단할 수 있다.
    • 컴파일러가 안전하지 않은 형변환을 감지하면 컴파일 오류를 발생시킨다.
    • 단, 상속 관계의 클래스나 구조체에 대한 검사는 불가능하다.
  2. 가독성
    • 코드의 가독성을 향상시키기 위해 명시적 자료형을 기제한다.
    • 다른 형변환 연산자들에 비해 명시적이고 읽기 쉬운 코드를 작성할 수 있다.
  3. 다른 형변환 연산자와 구분
    • static_cast는 다른 C++ 형변환 연산자와 구분하기 위해 사용됩니다.
    • 예를 들어, dynamic_cast는 실행 시간에 안전한 형변환이지만, 런타임 타입 정보(RTTI)를 필요로 하며, reinterpret_cast는 메모리 레이아웃을 변경하는 비안전한 형변환입니다.
  4. 컴파일 타임 값 계산
    • static_cast는 컴파일 타임 값 계산을 허용하며, 상수 표현식의 결과를 형변환하는 데 사용될 수 있습니다.

 

static_cast
#include <iostream>
using namespace std;

struct Base {
    int a = 1;
    virtual ~Base() {}
};

struct Drv1 : public Base {
    float x = 3.14;
};

struct Drv2 : public Base {
    int y = 3;
};

int main() {
    Base *b = new Drvl;
    
    Drvl *d1 = static_cast<Drv1 *>(b);
    Drv2 *d2 = static_cast<Drv2 *>(b);
    
    cout << dl->x << endl;
    
    delete b;

    return 0;
}

 

위 코드에서 main함수의 d2 포인터 변수는 Drv1 객체가 업캐스팅된 Base 포인터 객체를 Drv2로 변환하여 사용하려고 한다. 이것이 앞서 말한 컴파일러가 확인할 수 없는 상속 관계의 잘못된 형변환의 예시이며, 현재 하려는 다운캐스팅이 정말 유효한지를 잘 따져서 사용하는 것이 개발자의 몫이고, 정말 유효하다는확신이 있을 때만 static_cast를사용해야한다

 

잘못된 다운캐스팅 예방
#include <iostream>
using namespace std;

struct Base {
    int a = 1;
    virtual ~Base() {}
};

struct Drv1 : public Base {
    void f() {
        cout << "Drvl::f()" << endl;
        cout << x << endl;
    }
    float x = 3.14;
};

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

int main() {
    Base *b = new Drv1;
    Drv1 *d1 = static_cast<Drv1 *>(b);
    Drv2 *d2 = static_cast<Drv2 *>(b);

    if (d1) {
        d1->f();
    } else {
        cout << "Failed to cast to Drvl." << endl;
    }

    if (d2) {
        d2->f();
    } else {
        cout << "Failed to cast to Drv2." << endl;
    }

    delete b;

    return 0;
}
Drvl::f()
3.14
Drv2::f()
1078523331

 

위 코드에서 d2 변수는 분명 Drv1 객체를 업캐스팅하여 만든 b 변수를 다운캐스팅하였다. 하지만 결과적으로 어떠한 오류도 없이 출력문들이 나왔다.

 

이는 static_cast가 이루어지고 d1은 부모 클래스의 변수 a와 자식 클래스 변수 x를 포함하여 메모리에 할당된다.

※ 예로 메모리 할당 시작 주소는 100이다.

 

이제 static_cast가 이루어진 d2는 부모 클래스 변수 a와 자식 클래스 변수 y를 d1의 주소 값으로 바라보게 된다.

즉, 두 변수의 시작 주소를 그대로 사용하면서 float형 x의 이진법 형태 값을 그대로 int형 y로 읽어버리는 것이다.

 

 

하여 이진법으로 3.14라는 소수값을 정수로 그대로 옮긴 값이 출력된다.

3.14  = 01000000 01001000 11110101 11000011
float형 int형
3.14 1078523331

 

 

 


RTTI와 dynamic_cast

dynamic_cast는 동적으로 일어나는 형변환을 말한다. 즉, 프로그램 실행 도중 다운캐스팅을 하겠다는 의미이다.

이는 런타임에 올바른 캐스팅이 발생했는지 직접 검사하고, 올바른 경우에만 성공적으로 캐스팅을 진행한다는 말이다. 중요한 것은 이 모든 검사가 런타임에서 일어나기에 객체의 실체 타입에 대한 정보를 얻어올 수 있다.

 

dynamic_cast는 모든 타입에 사용할 수 있는 것은 아니다. 오로지 '다형적 클래스'(polymorphic class)끼리만 타입변환이 가능하다.

 

📌 다형적 클래스
가상 소멸자 또는 가상 함수가 하나라도 있는 클래스

 

다형적 클래스에서 가상 함수를 통해 동적 바인딩을 실현할 수 있는 이유는 가상 함수가 'RTTI'(Run-Time Type Information/Identification)이라는 메커니즘에 의해 작동되기 때문이다.

 

RTTI

RTTI는 런타임에 타입에 대한 정보를 가져오는 기능이다. 다형적 클래스의 타입을 가지는 객체를 만들면 그 객체의 실제 타입에 관한 정보를 가리키는 포인터가 암묵적으로 같이 들어간다. 즉, 가상 함수가 호출되면 암묵적 포인터의 객체로 가서 실제 타입을 알아낸다.

 

 

RTTI는 가상 함수를 적 바인딩하는 데도 사용되지만 dynamic_cast에도 사용된다. dynamic_cast가 다형적 클래스에만 사용이 가능한 이유이기도 하다.

 

다르게 말하면 dynamic_cast는 런타임 중 RTTI를 사용해야만 사용이 가능하며 다형적 클래스에서만 사용가능한 목적으로 존재한다.

 

  1. 런타임 형변환
    • dynamic_cast는 런타임에 객체의 실제 타입을 확인하고, 안전하게 형변환을 수행한다.
    • 이를 통해 다운캐스팅 시 타입 검사를 할 수 있다.
  2. 안전한 형변환
    • dynamic_cast는 런타임에 안전성을 확인하므로, 잘못된 형변환을 방지하기 위해 사용된다.
    • 형변환에 사용되는 클래스는 반드시 가상 함수를 가져야 한다.
  3. 포인터와 참조 형변환
    • 주로 다운캐스팅에서 사용되며, 클래스 간의 상속 관계가 있어야 한다.
  4. 다운캐스팅 실패 시 nullptr 반환
    • 만약 다운캐스팅이 실패하면, dynamic_cast는 포인터에서 nullptr를 반환합니다.

 

#include <iostream>

using namespace std;

// 기본 클래스
class Animal {
public:
    virtual void speak() const {
        cout << "Animal speaks" << endl;
    }
    virtual ~Animal() {}
};

// 파생 클래스 1
class Dog : public Animal {
public:
    void speak() const override {
        cout << "Dog barks: Woof! Woof!" << endl;
    }
};

// 파생 클래스 2
class Cat : public Animal {
public:
    void speak() const override {
        cout << "Cat meows: Meow! Meow!" << endl;
    }
};

int main() {
    Animal* animalPtr = new Dog;

    // dynamic_cast를 사용하여 다운캐스팅 시도
    Dog* dogPtr = dynamic_cast<Dog*>(animalPtr);
    Cat* catPtr = dynamic_cast<Cat*>(animalPtr);

    if (dogPtr) {
        // 다운캐스팅이 성공한 경우
        cout << "Casting to Dog successful." << endl;
        dogPtr->speak();
    } else {
        // 다운캐스팅이 실패한 경우
        cout << "Casting to Dog failed." << endl;
    }

    if (catPtr) {
        // 다운캐스팅이 성공한 경우
        cout << "Casting to Cat successful." << endl;
        catPtr->speak();
    } else {
        // 다운캐스팅이 실패한 경우
        cout << "Casting to Cat failed." << endl;
    }

    delete animalPtr;

    return 0;
}
Casting to Dog successful.
Dog barks: Woof! Woof!
Casting to Cat failed.


위 코드에서 Animal 클래스를 기반으로 Dog, Cat 파생 클래스가 생성되고, Animal 클래스는 가상 함수 speak를 포함한다. 즉, Animal 클래스는 다형성 클래스의 조건을 충족한다.

 

이제 main에서 Dog 클래스를 업캐스팅 받은 Animal 객체를 dynamic_cast를 통해 런타임에서 Dog와 Cat 객체로 각각 dynamic_cast 변환을 시도한다.

 

결과적으로 원래 Dog 객체를 업캐스트 받았기에, Dog 객체는 문제없이 dynamic_casting이 적용되었고 반대로 Cat 객체는 Dog 클래스를 받을 수 없기에 nullptr가 반환되었다.

 

 

dynamic_cast 자체의 성능은 좋은 편은 아니다. 하여 자주 사용되지는 않는다. 반대로 가상 함수를 잘 설계하면 dynamic_cast를 할 필요가 없어지는 경우도 많다.

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함