1. C++에서의 형 변환 연산
C언어의 형 변환 연산자는 강력하기 때문에, 아래 예제에서 보이는 실수를 해도 컴파일러는 이를 잡아내지 못한다.
class car { private: int fuelgauge; public: car(const int fuel) : fuelgauge(fuel) {} void showcarstate(void) const { cout << "잔여 연료량 : " << fuelgauge << endl; } }; class truck : public car { private: int freightweight; public: truck(const int fuel, const int w) : car(fuel), freightweight(w) {} void showtruckstate(void) const { showcarstate(); cout << "화물의 무게 : " << freightweight << endl; } }; int main(void) { car *pcar1 = new truck(80, 200); truck *ptruck1 = (truck*)pcar1; ptruck1->showtruckstate(); cout << endl; car *pcar2 = new car(120); truck *ptruck2 = (truck*)pcar2; ptruck2->showtruckstate(); return 0; } |
잔여 연료량 : 80 화물의 무게 : 200 잔여 연료량 : 120 화물의 무게 : -33686019 |
두 번째 형 변환의 경우, 조금만 분석해보면 프로그래머의 실수임을 판단할 수 있지만,
첫 번째 형 변환의 경우, 실수인지 아닌지를 쉽게 판단할 수 없다. 결과만 놓고보면 컴파일도 실행결과에도 문제가 없지만,
불필요한 형 변환이기 때문이다.
이러한 유형의 논란과 문제점 때문에 C++에서는 다음과 같이 총 4개의 연산자를 추가로 제공한다.
- static_cast
- const_cast
- dynamic_cast
-reinterpret_cast
위의 형 변환 연산자들을 사용하면 프로그래머는 자신이 의도한 바를 명확히 표시할 수 있다.
따라서 컴파일러도 실수를 지적해줄 수 있고, 코드를 직접 작성하지 않은 프로그래머들도 실수 여부를 판단할 수 있다.
1.1 dynamic_cast : 상속관계에서의 안전한 형 변환
dynamic_cast 형 변환 연산자는 다음의 형태를 갖는다.
=> dynamic_cast<T>(expr)
즉, '<>' 사이에 변환하고자 하는 자료형의 이름을 두되, 객체의 포인터 또는 참조형이 와야하며,
'()' 사이에는 변환의 대상이 와야 한다. 그리고 요구한 형 변환이 적절한 경우에는 형 변환된 데이터를
반환하지만, 적절하지 않은 경우에는 컴파일 시 에러가 난다. 여기서 말하는 적절한 형 변환은 다음과 같다.
"상속관계에 놓여 있는 두 클래스 사이에서 유도 클래스의 포인터 및 참조형 데이터를, 기초 클래스의
포인터 및 참조형 데이터로 형 변환하는 경우."
int main(void) { // compile error car *pcar1 = new truck(80, 200); truck *ptruck1 = dynamic_cast<truck*>(pcar1); // compile error car *pcar2 = new car(120); truck *ptruck2 = dynamic_cast<truck*>(pcar2); // compile success car *ptruck3 = new truck(70, 150); car *pcar3 = dynamic_cast<car*>(ptruck3); return 0; } |
사실 dynamic_cast 연산자를 사용했다는 것은 다음의 의미가 담겨있다.
"상속관계에 있는 유두 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환한다."
1.2 static_cast : A 타입에서 B 타입으로
static_cast 형 변환 연산자는 다음의 형태를 갖는다(dynamic_cast 연산자와 동일한 형태이다).
=> static_cast<T>(expr)
static_cast 연산자를 사용하면, 유도 -> 기초 클래스로 형 변환 뿐만 아니라, 기초 -> 유도 클래스로 형 변환이
가능하다. 하지만 그에 대한 책임은 프로그래머가 져야 한다.
따라서, static_cast는 dynamic_cast 연산자와 달리, 보다 많은 형 변환을 허용한다. 그러나 dynamic_cast를 사용할 수 있는
경우에는 해당 연산자를 사용하여 안정성을 높여야 한다.
또한, static_cast 형 변환 연산자는 기존 자료형 간의 형 변환도 허용한다.
그렇다면, C언어의 형 변환 연산자와의 차이점이 없을 수도 있으나, 아래 예제를 참고하자.
int main(void) { const int num = 20; int *ptr = (int*)# *ptr = 30; cout << *ptr << endl; float *adr = (float*)ptr; cout << *adr << endl; return 0; } |
30 4.2039e-044 |
위의 예제에서 보인 형 변환은, static_cast 연산자로는 불가능한 형 변환이다. 따라서 여전히 static_cast는
C언어의 형 변환 연산자보다 적은 것을 허용하고 있다. 이로 인해 static_cast를 보는 순간 다음과 같이 판단할 수 있다.
"상속관계에 있는 클래스의 포인터 및 참조형 데이터의 형 변환인가? 아니면 기본 자료형 데이터의 형 변환인가?"
1.3 const_cast : const 성향을 삭제하라
C++에서는 포인터와 참조자의 const 성향을 제거하는 형 변환을 목적으로, 다음의 형 변환 연산자를 제공한다.
=> const_cast<T>(expr)
사실 이러한 연산은 const의 가치를 떨어뜨리는 것이라고 생각하는 분들도 계시지만, 그 이면을 살펴보면,
나름의 의미를 발견할 수 있다.
주로, const_cast 형 변환 연산은, 함수의 인자전달 시 const 선언으로 인한 형(type)의 불일치가 발생해서
인자의 전달이 불가능한 경우에 유용하게 사용이 된다.
1.4 reinterpret_cast : 상관없는 자료형으로의 형 변환
reinterpret_cast 연산자는 전혀 상관이 없는 자료형으로의 형 변환에 사용이 되며, 기본적인 형태는 다음과 같다.
=> reinterpret_cast<T><expr>
reinterpret_cast 연산자는 포인터를 대상으로 하는, 그리고 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다.
int main(void) { int num = 72; int *ptr= # // 주소 값을 정수로 변환 int adr = reinterpret_cast<int>(ptr); cout << "Addr : " << adr << endl; // 정수를 다시 주소 값으로 변환 int *rptr = reinterpret_cast<int*>(adr); cout << "value : " << *rptr << endl; return 0; } |
Addr : 11926080 value : 72 |
크게 사용 의미를 부여하긴 어렵겠지만, reinterpret_cast 연산자가 포인터와 관련이 있는 모든 유형의
형 변환을 허용한다는 사실으 뒷받침하기도 한다.
1.5 dynamic_cast 두 번째 이야기 : Polymorphic 클래스 기반의 형 변환
지금까지 설명한 내용을 잘 이해했다면, 상속과 관련된 형 변환에 대한 내용은 다음과 같다.
- 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는
dynamic_cast 연산자를 사용한다.
- 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는
static_cast 연산자를 사용한다.
그란 dynamic_cast 연산자도 기초 -> 유도 클래스의 포인터 및 참조형 데이터의 형 변환을 허용한다.
다음의 조건만 만족하면 말이다.
=> 기초 클래스가 'Polymorphic 클래스'이다.
즉, 하나 이상의 가상 함수를 지니는 클래스와 상속 관계에 놓여있으면, dynamic_cast 연산자를 이용하여
기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 변환이 가능하다.
class A { public: virtual void show(void) { cout << "A's show" << endl; } }; class B : public A { public: void show(void) { cout << "B's show" << endl; } }; int main(void) { A *aptr = new B; B *bptr = dynamic_cast<B*>(aptr); bptr->show(); return 0; } |
B's show |
만약 A 클래스의 함수가 'virtual'로 선언되어 있지 않았다면 에러가 발생했을 것이다.
위의 연산이 성공한 이유는, 포인터 변수 'aptr'이 실제 가리키는 객체가 B 객체이기 때문이다.
즉, aptr이 가르키는 객체를 B 클래스형 포인터 변수가 함께 가리켜도 문제가 되지 않기 때문에 성공한 것이다.
그럼 다음 예제를 보도록 하자.
int main(void) { A *aptr = new A; B *bptr = dynamic_cast<B*>(aptr); bptr->show(); return 0; } |
위의 경우에는 aptr이 가르키는 대상을 bptr이 가르킬 수 없는 상황이다. 따라서 이러한 경우에는
형 변환의 결과로 NULL 포인터가 반환된다.
즉, dynamic_cast는 안정적인 형 변환을 보장한다. 특히 컴파일 시간이 아닌 실행시간에 안정성을 검사하도록
컴파일러가 바이너리 코드를 생성한다는 점에 주목할 필요가 있다.
그만큼 실행속도가 늦어지지만, 그만큼 안정적인 형 변환이 가능한 것이다.
반대로 static_cast의 경우에는, 컴파일러는 무조건 형 변환이 되도록 바이너리 코드를 생성하기 때문에,
실행속도는 빠르지만, 그로 인한 실행의 결과는 프로그래머가 책임져야 한다.
1.6 bad_cast 예외
dynamic_cast 연산자를 이용한 형 변환의 과정에서 발생할 수 있는 예외이다.
class A { public: virtual void show(void) { cout << "A's show" << endl; } }; class B : public A { public: void show(void) { cout << "B's show" << endl; } }; int main(void) { A a1; A &aref = a1; try { B &bref = dynamic_cast<B&>(aref); } catch(bad_cast expt) { cout << expt.what() << endl; } return 0; } |
Bad dynamic_cast! |
위의 예제에서는 aref가 실제 참조하는 객체가 A객체이기 때문에, B의 참조형으로 참조하는 것은
안전하지 못하다. 참조형을 대상으로 dynamic_cast 연산을 진행할 경우에 참조자를 대상으로는 NULL을
반환할 수 없기 때문에 이러한 상황에서는 'bad_cast' 예외가 발생한다.
출처 : 윤성우 열혈 C++ 프로그래밍
'프로그래밍 언어들 > C++' 카테고리의 다른 글
CHAPTER15 - 예외처리(Exception Handling) (0) | 2016.11.28 |
---|---|
CHAPTER14 - 템플릿(Template) 2 (0) | 2016.11.25 |
CHAPTER13 - 템플릿(Template) 1 (0) | 2016.11.25 |
CHAPTER12 - String 클래스의 디자인 (0) | 2016.11.24 |
CHAPTER11 - 연산자 오버로딩 2 (0) | 2016.11.23 |