제가 상속관계에서 형변환을 할 때와 virtual 유무에 대해 햇갈렸던 부분들을 정리한 글입니다.
(틀린 부분있다면 말해주시면 감사하겠습니다)
먼저 형변환의 방법으로는
int a = 10;
float b = (float)a; // 옛날 C 스타일 (구식)
float c = float(a); // C++ 스타일 (신식)
위와 같은 방법과
C++의 형변환 연산자 아래의 4가지가 있습니다.
- static_cast : 기본 자료형 간의 형변환과 클래스 상속관계에서의 형변환만 허용한다. 명시적 형변환 (C스타일 보다 안전하다)
- const_cast : 포인터와 참조자의 const 성향을 제거하는 형 변환
- dynamic_cast : 상속관계에서의 안전한 형변환
- reinterpret_cast : 상관없는 자료형으로의 형변환
이중에서도 dynamic_cast는
"상속관계에 놓여있는 두 클래스 사이에서 자식클래스의 포인터 및 참조형 데이터를 부모 클래스의 포인터 및 참조형 데이터로 형변환 하는 경우"에 사용할 수 있습니다.
class A
{
private:
int num_a;
public:
explicit A(int n = 1) : num_a(n) {}
void PrintNum() { cout << num_a << endl; }
};
class B : public A
{
private:
int num_b;
public:
explicit B(int n = 2) : A(1), num_b(n) {};
void PrintNum() { cout << num_b << endl; }
};
int main()
{
using namespace std;
A* pa = new B(2);
B* tempB = dynamic_cast<B*>(pa); // Compile Error
A* pa2 = new A(1);
B* tempB2 = dynamic_cast<B*>(pa2); // Compile Error
B* tempB3 = new B(2);
A* pa3 = dynamic_cast<A*>(tempB3); // Compile OK
return 0;
}
즉, 위의 코드에서 3번째 부분만 컴파일이 통과하는 것을 볼 수 있습니다.
저는 처음에 위의 코드를 보고 왜 컴파일 에러가 나는지 의야해 하며
dynamic_cast가 상속관계에서 형변환을 할 때, "만능"? 느낌으로 다 사용할 수 있는것으로 알고 대충 넘어갔었는데
최근에 책으로 공부하면서 제가 잘 모르고 넘어갔던 부분들이 보였습니다.
두번째 형변환의 경우에는 당연히 객체가 A이기 때문에 B*로 형변환 하는것은 말이 안되기 때문에 바로 이해가 갑니다.
또한 세번째의 경우에도 "상속관계에 놓여있는 두 클래스 사이에서 자식클래스의 포인터 및 참조형 데이터를 부모 클래스의 포인터 및 참조형 데이터로 형변환 하는 경우"에 dynamic_cast를 사용할 수 있기 때문에 말이 됩니다.
그러면 첫번째의 경우에 형변환을 할려면은 저희는 static_cast 형변환 연산자를 사용해야합니다.
(조금 위험하지만 현재 코드가 간단하고 pa가 B의 객체라는 것이 분명하기 때문에 허용합니다.)
int main()
{
using namespace std;
A* pa = new B(2);
B* tempB = static_cast<B*>(pa); // Compile OK
tempB->PrintNum();
A* pa2 = new A(1);
B* tempB2 = static_cast<B*>(pa2); // Compile OK
tempB2->PrintNum();
B* tempB3 = new B(2);
A* pa3 = dynamic_cast<A*>(tempB3); // Compile OK
return 0;
}
위와 같이 수정할 경우 첫번째의 경우 정상적으로 2가 출력이 되고 두번째의 경우 쓰레기 값이 출력이됩니다.
두번째의 경우에는 당연히 안전하지 않게 형변환을 진행한 후에 PrintNum을 호출하여 메모리를 침범했기때문에 발생하는 문제입니다.
첫번째의 경우 캐스팅이 잘되어 출력이 정상적으로 됩니다.
다만 "안전하지 않기 때문에" dynamic_cast를 사용하고 싶은데 그럴경우
클래스의 설계할 때 "하나 이상의 가상함수를 지니는 클래스"로 만들어 주시면 자식클래스 포인터형이나 참조형으로 안전하게 dynamic_cast 형변환 연산자를 사용할 수 있습니다.
class A
{
private:
int num_a;
public:
explicit A(int n = 1) : num_a(n) {}
virtual void PrintNum() { cout << num_a << endl; }
};
class B : public A
{
private:
int num_b;
public:
explicit B(int n = 2) : A(1), num_b(n) {};
virtual void PrintNum() override { cout << num_b << endl; }
};
int main()
{
using namespace std;
A* pa = new B(2);
B* tempB = dynamic_cast<B*>(pa); // Compile OK
tempB->PrintNum();
A* pa2 = new A(1);
B* tempB2 = static_cast<B*>(pa2); // Compile OK
tempB2->PrintNum();
B* tempB3 = new B(2);
A* pa3 = dynamic_cast<A*>(tempB3); // Compile OK
return 0;
}
즉, 위와같이 첫번째의 경우 dynamic_cast가 가능합니다.
A* pa = new B(2); 객체 생성시 pa가 가르키는 곳의 첫번째 주소에는 vftable의 주소가 들어가게 됩니다.
vftable은 아래의 링크에서 확인해주시면 됩니다.
https://cjbworld.tistory.com/13
vftable의 주소가 pa에 첫번째 주소에 들어가있기 때문에 dynamic_cast 형변환 연산을 진행하게 되면
"컴파일 시간이 아닌 실행시간(프로그램 이 실행중인 동안)에 안정성을 검사하도록 complier가 바이너리 코드를 생성" 하게됩니다. 즉, RTTI를 사용해서 캐스팅을 하는 것이지요. ("Run Time Type Information")
이러한 특성때문에 dynamic_cast는 dynamic이 이 붙는 것이고 static_cast는 안정성으로 보장하지 않고, 컴파일러가 무조건 형변환이 되도록 (실행중인 동안에 안정성 검사를 진행하지 않는 특성) 하기 때문에 static으로 시작합니다.
정리
int main()
{
using namespace std;
A* pa = new A();
B* pb = dynamic_cast<B*>(pa);
if (pb == nullptr)
{
cout << "nullptr" << endl;
}
return 0;
}
따라서 위와 같이 캐스팅을 하려고하면, dynamic_cast 형변환 연산자가 RTTI를 통해 안전하게 vftable부터 확인후에 맞는 타입인지 아닌지 확인을 다 끝내고 나면 캐스팅을 하거나 nullptr을 반환합니다.
런타임중에 실행하는 dynamic_cast보다 static_cast가 속도가 당연히 빨라 자신감?이 있다면 static_cast를 사용해도 괜찮을 거 같습니다.(실제로 static_cast를 사용한다는 분도 계신거같습니다..?)
다만 위와 같은경우 dynamic_cast를 사용하면 nullptr 을 반환하며 위험한 캐스팅을 방지할 수 있습니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!