상속에서의 형변환 - RTTI와 dynamic_cast
static_cast: 정적인 시간(컴파일 타임)에 캐스팅이 일어남
dynamic_cast : 동적인 시간(런타임)에 캐스팅이 일어남
복습! static cast
class Base {
public:
int x;
};
class Derived : public Base {
public:
int y;
};
int main() {
Base *b = new Derived;
Derived *d = static_cast<Derived*>(b);
}
- b가 가리키는 인스턴스의 타입을 알고 있다면, static_cast를 이용하여 형변환을 해줄 수 있었다
RTTI(Run Time Type Information/Identification)
b가 실제로 어떤 타입을 가리키는지 컴파일 타임에 알기 어려운 경우가 많다
- 런타임에 의존하는 경우가 발생
런타임에 포인터가 가리키는 객체의 타입을 알 수 있게끔 해주는 기능을 RTTI라고 한다
사실 C++에서 기본적으로 포인터가 가리키는 객체의 타입을 런타임에 확인할 수 있는 방법은 없다
그런데 C++ 내부에서 RTTI가 필요할 수 밖에 없는 경우가 딱 한 가지 존재한다
- 가상함수를 사용했을 경우이다
class Base {
public:
void f() {}
int x;
};
class Derived : public Base {
public:
void f() {}
int y;
};
int main() {
Base *b = new Derived;
Derived *d = static_cast<Derived*>(b);
b->f();
}
- 이 경우에는 f()가 가상 함수로 선언되어있지 않기 때문에 정적 바인딩이 일어난다
class Base { // 다형 클래스
public:
virtual void f() {}
int x;
};
class Derived : public Base {
public:
void f() {}
int y;
};
int main() {
Base *b = new Derived;
Derived *d = static_cast<Derived*>(b);
b->f();
}
만약 virtual로 f()를 선언하면 컴파일러는 b->f()를 건들지 않는다
이게 가능하려면 런타임에 b가 가리키는 객체의 타입이 무엇인지를 알아올 수 있는 방법이 있어야 동적 바인딩이 가능하다
RTTI가 필요하다.
가상 함수가 하나라도 있는 클래스를 다형 클래스라고 한다
일단 virtual을 빼서 다형 클래스가 아닌 클래스로 만들어보자
#include <iostream>
using namespace std;
class Base {
public:
void f() {}
int x;
};
class Derived : public Base {
public:
void f() {}
int y;
};
int main() {
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;
}
Base의 크기는 4바이트, Derived의 크기는 8바이트로 예상할 수 있다.
여기서 virtual을 추가하면
#include <iostream>
using namespace std;
class Base {
public:
void f() {}
int x;
};
class Derived : public Base {
public:
void f() {}
int y;
};
int main() {
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;
}
크기가 8, 12로 출력된다. 4바이트가 더 늘어났다.
이 4바이트는 기본적으로 어딘가를 가리키는 포인터다. 이 포인터에 클래스에 대한 정보가 담겨있다.
Derived가 생성되면, Derived 타입의 정보를 담고 있는 메모리 공간을 4바이트 포인터가 가리키게 된다.
Base가 생성되면, Base 타입의 정보를 담고 있는 메모리 공간을 4바이트 포인터가 가리키게 된다.
Base *b = new Derived; 라면 어떻게 동작할까?
포인터 b는 Base* 이므로 8바이트 공간을 가리키는 포인터이다.
여기서 RTTI를 실현하기 위해, 4바이트 포인터가 가리키는 곳을 따라가면 Derived라는 타입의 정보를 담는 공간이 나온다.
그래서, 실제로 가리키는 객체가 Base처럼 생겼지만 실제로는 Derived 타입이라는 것을 확인한다.
이것이 기본적인 RTTI의 원리이다.
Dynamic cast
dynamic_cast는 런타임에 타입에 대한 정보를 가지고 있어야 하기 때문에, RTTI가 지원되는 경우에만 dynamic cast를 사용해서 다운 캐스팅을 할 수가 있는 것이다.
따라서 Base로부터 Derived로 dynamic cast를 하고 싶다면, Base 클래스는 반드시 가상 함수를 포함하는 다형 클래스여야 한다
int main() {
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;
int *a = (int*)new Base;
cout << a[0] << " " << a[1] << endl;
int *b = (int*)new Derived;
cout << b[0] << " " << b[1] << " " << b[2] << endl;
int *c = (int*)new Derived;
cout << c[0] << " " << c[1] << " " << c[2] << endl;
delete a;
delete b;
delete c;
}
(int*)new Base는 8바이트 메모리 공간을 가리키는 포인터가 첫 4바이트를 보라는 의미
- a[0]는 첫 4바이트 공간, a[1]은 두 번째 4바이트 공간
실행결과
8
12
2661172 10
2661184 10 20
2661184 10 20
- 첫 4바이트 공간에는 2661172(Base 클래스 정보), 2661184(Derived 클래스 정보)가 들어가 있다.
Example) Shaple Class
#include <iostream>
#include <math.h>
using namespace std;
class Shape {
public:
virtual double GetArea() const = 0;
virtual void Resize(double k) = 0;
};
class Circle : public Shape {
public:
Circle(double r) : r(r) {}
double GetArea() const {
return r * r * 3.14;
}
void Resize(double k) {
r *= k;
}
private:
double r;
};
class Rectangle : public Shape {
public:
Rectangle(double a, double b) : a(a), b(b) {}
double GetArea() const {
return a * b;
}
void Resize(double k) {
a *= k;
b *= k;
}
double GetDiag() const {
return sqrt(a * a + b * b);
}
private:
double a, b;
};
int main() {
Shape *shapes[] = { new Circle(1), new Rectangle(1, 2) }; // 포인터 배열
for (int i = 0; i < 2; i++) {
// 도형의 넓이
// 만약 직사각형일 경우, 대각선 길이 출력
cout << "도형의 넓이:" << shapes[i]->GetArea() << endl;
Rectangle *r = dynamic_cast<Rectangle*>(shapes[i]);
}
for (int i = 0; i < 2; i++) {
delete shapes[i];
}
}
만약 shapes를 Rectangle로 static_cast를 했다면 shape를 Rectangle로 가정해서 캐스팅하기 때문에 컴파일 과정에서는 오류가 발생하지 않았을 것이다.
하지만 r이 실제로 가리키는 객체가 Rectangle인지 Circle인지는 런타임에만 알 수 있으므로 dynamic_cast를 사용하자
dynamic cast를 사용하면 가리키는 객체가 Rectangle일때만 캐스팅에 성공한다
- 형변환에 실패하면 NULL이 r에 들어가게 된다
그래서 if문에서 NULL에 대한 처리를 추가해줘야 한다.
int main() {
Shape *shapes[] = { new Circle(1), new Rectangle(1, 2) };
for (Shape *s : shapes) {
// 도형의 넓이
// 만약 직사각형일 경우, 대각선 길이 출력
cout << "도형의 넓이:" << s->GetArea() << endl;
Rectangle *r = dynamic_cast<Rectangle*>(s);
if (r! = NULL) {
cout << "대각선의 길이:" << s->GetDiag() << endl;
}
}
for (Shape *s : shapes) {
delete s;
}
}
- Dynamic cast는 RTTI 작업을 거쳐야 하기 때문에 성능 측면에서 좋지는 않다
Dynamic cast를 사용하지 않고 Shape Class 구현
#include <iostream>
#include <string>
#include <math.h>
using namespace std;
class Shape {
public:
virtual ~Shape() {}
virtual double GetArea() const = 0;
virtual void Resize(double k) = 0;
virtual string GetInfo() const {
return "도형의 넓이 : " + to_string(GetArea())
}
};
class Circle : public Shape {
public:
Circle(double r) : r(r) {}
double GetArea() const {
return r * r * 3.14;
}
void Resize(double k) {
r *= k;
}
private:
double r;
};
class Rectangle : public Shape {
public:
Rectangle(double a, double b) : a(a), b(b) {}
double GetArea() const {
return a * b;
}
void Resize(double k) {
a *= k;
b *= k;
}
double GetDiag() const {
return sqrt(a * a + b * b);
}
string GetInfo() const {
return Shape::GetInfo() + "\n대각선의 길이: " + to_string(GetDiag());
}
private:
double a, b;
};
int main() {
Shape *shapes[] = { new Circle(1), new Rectangle(1, 2) };
for (Shape *s : shapes) {
// 도형의 넓이
// 만약 직사각형일 경우, 대각선 길이 출력
cout << "도형의 넓이:" << s->GetArea() << endl;
Rectangle *r = dynamic_cast<Rectangle*>(s);
if (r! = NULL) {
cout << "대각선의 길이:" << s->GetDiag() << endl;
}
}
for (Shape *s : shapes) {
delete s;
}
}
- GetInfo 함수를 가상 함수로 만들어서 각 인스턴스의 타입에 맞는 정보를 출력하도록 구현하였다.
참조) 두들낙서 C/C++ 강좌
'Programming Language > C++' 카테고리의 다른 글
[C++] 다중 상속과 다이아몬드 문제 (0) | 2024.06.28 |
---|---|
[C++] 객체 지향 프로그래밍의 4대 원칙(원리) (0) | 2024.06.28 |
[C++] 상속에서의 형변환 - 다운캐스팅 (0) | 2024.06.27 |
[C++] 상속에서의 형변환 - 업캐스팅(Upcasting) (0) | 2024.06.27 |
[C++] 생성/소멸자 실행 순서와 가상 소멸자 (0) | 2024.06.27 |