상속에서의 형변환 - 다운캐스팅(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++ 강좌
'Programming Language > C++' 카테고리의 다른 글
[C++] 객체 지향 프로그래밍의 4대 원칙(원리) (0) | 2024.06.28 |
---|---|
[C++] 상속에서의 형변환 - RTTI와 dynamic_cast (0) | 2024.06.28 |
[C++] 상속에서의 형변환 - 업캐스팅(Upcasting) (0) | 2024.06.27 |
[C++] 생성/소멸자 실행 순서와 가상 소멸자 (0) | 2024.06.27 |
[C++] 순수 가상 함수와 추상 클래스 (0) | 2024.06.27 |