[문과 코린이의 IT 기록장] C++ - C++에서의 형 변환 연산 (C에서의 형 변환 연산의 강력함, static_cast, const_cast, dynamic_cast, reinterpret_cast)
1. C에서의 형 변환 연산의 강력함
- C++에서는 C스타일의 형 변환 연산자를 가리켜 '오래된 C스타일 형 변환 연산자(Old C-style cast operator)'라 부름
- 즉, C스타일의 형 변환 연산자는, C언어와의 호환성을 위해 존재할 뿐, C++에서는 새로운 형 변환 연산자와 규칙을 제공하고 있다.
Case 1 )
- C언어는 강력해서 변환하지 못하는 대상이 없다. 따라서, 아래와 같은 실수를 해도 컴파일러는 잡아내지 못한다.
#include <iostream>
using namespace std;
class Car {
private:
int fuelGuage;
public:
Car(int fuel) :fuelGuage(fuel) {}
void ShowCarState() { cout << "잔여 연료량 : " << fuelGuage << endl; }
};
class Truck : public Car {
private:
int freighWeight;
public:
Truck(int fuel, int weight) : Car(fuel), freighWeight(weight) {}
void ShowTruckState() {
ShowCarState();
cout << "화물의 무게 : " << freighWeight << endl;
}
};
int main() {
Car* pcar1 = new Truck(80, 200);
// 포인터변수 pcar1이 가리키는 대상이 실제로는 Truck객체임
Truck* ptruck1 = (Truck*)pcar1; // 문제 없어보이는 형변환
// 따라서, 이 형변환 연산은 문제가 되지 않을 수 있다.
// 그러나 기초 클래스의 포인터 형을 유도 클래스의 포인터 형으로 변환하는 것은 일반적이지 않으며, 이 상황이 프로그래머의 의도인지 실수인지 알 방법이 없다.
ptruck1->ShowTruckState();
cout << endl;
Car* pcar2 = new Car(120);
Truck* ptruck2 = (Truck*)pcar2; // 문가 없어보이는 형변환
// 포인터 변수 pcar2가 가리키는 대상이 실제로 Car객체임
// 따라서, 이 형변환 연산은 문제가 됨.
// 그러나 C스타일의 이러한 형 변환 연산자는 컴파일러로 하여금 이러한 일이 가능하게 함
ptruck2->ShowTruckState();
// 따라서 이 실행결과는 예측이 불가능함.
// 즉 ptruck2가 가리키는 대상은 Car객체에기 때문에 ShowTruckState()호출은 논리적으로 맞지 않음.
// 더불어 이 객체는 화물의 무게를 의미하는 맴버 freightWeiht가 존재하지 않음.
return 0;
}
이러한 유형의 논란과 문제점 때문에, C++에서는 4가지 연산자를 추가로 제공하면서 용도에도 맞는 형 변환 연산자의 사용을 돕고 잇음
- static_cast
- const_cast
- dynamic_cast
- reinterpret_cast
이 형변환 연산자들을 사용하면 프로그래머는 자신이 의도한 바를 명확히 표시할 수 있다.
따라서, 컴파일러도 프로그래머의 실수를 지적해 줄 수 있고, 코드를 직접 작성하지 않은 프로그래머들도 코드를 직접 작성한 프로그래머의 실수여부를 판단할 수 있다.
2. dyanimic_cast : 상속관계에서의 안전한 형 변환
[ dyanamic_cast 형 변환 연산자의 형태 ]
dynamic_cast<T>(expr)
// T : 반환하고자 하는 자료형의 이름 (객체의 포인터 or 참조형이 와야 함)
// expr : 변환의 대상 (변환이 적절한 경우에는 형 변화된 데이터를 반환하고, 적절하지 않은 경우에는 컴파일 에러 발생)
적절한 변환이란? )
상속관계에 놓여있는 두 클래스 사이에서, 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 변환하는 경우.
Case 2 )
#include <iostream>
using namespace std;
class Car {
private:
int fuelGuage;
public:
Car(int fuel) :fuelGuage(fuel) {}
void ShowCarState() { cout << "잔여 연료량 : " << fuelGuage << endl; }
};
class Truck : public Car {
private:
int freighWeight;
public:
Truck(int fuel, int weight) : Car(fuel), freighWeight(weight) {}
void ShowTruckState() {
ShowCarState();
cout << "화물의 무게 : " << freighWeight << endl;
}
};
int main() {
Car* pcar1 = new Truck(80, 200);
Truck* ptruck = dynamic_cast<Truck*>(pcar1); // 컴파일 에러
Car* pcar2 = new Car(120);
Truck* ptruck2 = dynamic_cast<Truck*>(pcar2); // 컴파일 에러
Truck* ptruck3 = new Truck(70, 150);
Car* pcar3 = dynamic_cast<Car*>(ptruck3); // 컴파일 OK
return 0;
}
3. static_cast : A타입에서 B타입으로
[ static_cast 형 변환 연산자의 형태 ]
static_cast<T>(expr)
// T : 반환하고자 하는 자료형의 이름 (객체의 포인터 or 참조형이 와야 함)
// expr : 변환의 대상 (변환이 적절한 경우에는 형 변화된 데이터를 반환하고, 적절하지 않은 경우에는 컴파일 에러 발생)
적절한 변환 )
유도 클래스의 포인터 및 참조형 데이터를, 기초 클래스의 포인터 및 참조형 데이터로뿐만 아니라,
기초 클래스의 포인터 및 참조형 데이터도 유도 클래스의 포인터 및 참조형 데이터로,
아무 조건 없이 형 변환 시켜줌.
Case 3 )
#include <iostream>
using namespace std;
class Car {
private:
int fuelGuage;
public:
Car(int fuel) :fuelGuage(fuel) {}
void ShowCarState() { cout << "잔여 연료량 : " << fuelGuage << endl; }
};
class Truck : public Car {
private:
int freighWeight;
public:
Truck(int fuel, int weight) : Car(fuel), freighWeight(weight) {}
void ShowTruckState() {
ShowCarState();
cout << "화물의 무게 : " << freighWeight << endl;
}
};
int main() {
Car* pcar1 = new Truck(80, 200);
Truck* ptruck1 = static_cast<Truck*>(pcar1);
// 포인터 pcar1을 Truck의 포인터 형으로 변환하겠다.
// 이 포인터가 Truck객체를 가르키기 때문에 문제는 없음
ptruck1->ShowTruckState();
cout << endl;
Car* pcar2 = new Car(120);
Truck* ptruck2 = static_cast<Truck*>(pcar2);
// 포인터 pcar2를 Truck의 포인터 형으로 변환하겠다.
// 이 포인터가 Car 객체를 가리키는 것은 옳은 상황이 아님. (이러한 형태로 문장을 구성 X)
ptruck2->ShowTruckState();
return 0;
}
static_cast연산자는 dynamic_cast 연산자와 달리, 보다 많은 형 변환을 허용하지만, 이러한 오류가능성 때문에 프로그래머는 신중하게 선택해야 한다.
danamic_cast 연산자를 사용할 수 있는 경우에는 이 연산자를 사용해 안정성을 높여야 하며, 그 이외의 경우에는 정말 책임질 수 있는 상황에서만 제한적으로 static_cast 연산자를 사용해야 한다.
[ static_cast 연산자 : 기본 자료형 데이터의 형 변환 ]
int main()
{
int num1 = 20, num2 = 3; // 정수형 나눗셈
double result = 20/3; // 실수형을 실행하고 싶지만 정수형 나눗셈의 결과로 출력됨
cout<<result<<endl;
...
}
이 문장을 실수형 나눗셈을 진행하도록 만드는 세 가지 방법
double result = (double)20/3; // 1번째 방법
double result = double(20)/3; // 2번째 방법
double result = static_cast<double>(20)/3; // 3번째 방법
* static_cast 연산자는 '기본 자료형 간의 형 변환'과 '클래스의 상속관계에서의 형 변환'만 허용한다. C언어의 형 변환 연산자는 일반적이지 않은 형 변환도 허용하기 때문에 이 연산자의 사용은 여전히 의미를 지닌다.
// 말도 안 되는 C언어 형변환 연산
int main()
{
const int num = 20; // const 상수
int *ptr = (int*)# // const상수의 포인터는 const 포인터이다
*ptr = 30; // const 상수 num의 값이 실제로 변경된다. (문제)
cout<<*ptr<<endl; // 30출력
float * adr = (float*)ptr; // int형 포인터를 float형으로 변환한다.
cout<<*adr<<endl; // 저장된 데이터를 float형으로 해석해서 출력한다.
...
}
3. const_cast : const의 성향을 삭제하라
C++에서는 참조자의 const 성향을 제거하는 형 변환을 목적으로, 다음의 형 변환 연산자를 제공하고 있다.
[ const_cast 형 변환 연산자의 형태 ]
const_cast<T>(expr)
// T : 반환하고자 하는 자료형의 이름 (객체의 포인터 or 참조형이 와야 함)
// expr : 변환의 대상 (변환이 적절한 경우에는 형 변화된 데이터를 반환하고, 적절하지 않은 경우에는 컴파일 에러 발생)
Case 4 )
이 연산자를 이용해서 const로 선언된 참조자, 그리고 포인터의 const성향을 제거하는 것은, const의 가치를 떨어뜨리는 것 처럼 보여도 나름의 의미를 지닌다.
#include<iostream>
using namespace std;
void ShowString(char* str) { // 문자열을 인자로 받는 함수
cout << str << endl;
}
void ShowAddResult(int& n1, int& n2) { // 인자로 전달되는 int형 변수를, 참조형으로 전달받는 함수
cout << n1 + n2 << endl;
}
int main() {
const char* name = "Lee Sung Ju";
// 포인터 변수 name은 const char*형
ShowString(const_cast<char*>(name));
// ShowString()의 매개변수는 char*형
// 따라서, (name)은 매개변수의 인자로 전달될 수 없음.
// 그러므로 const 성향을 가진, 포인터 변수 name의 const가치를 제거해서 전달해줌
const int& num1 = 100;
const int& num2 = 200;
ShowAddResult(const_cast<int&>(num1), const_cast<int&>(num2));
// const int&형 데이터를, int&형 데이터로 변환하고 있음
return 0;
}
이 사례와 같이 const_cast형 변환 연산은, 함수의 인자전달시 const 선언으로 인한 형(type)의 불일치가 발생해, 인자의 전달이 불가능한 경우에 유용하게 사용된다.
4. reinterpret_cast : 상관없는 자료형으로의 형 변환
reinterpret_cast 연산자는, 전혀 상관이 없는 자료형으로의 형 변환에 사용이 되며, 기본적인 형태는 아래와 같다.
reinterpret_cast<T>(expr)
// T : 반환하고자 하는 자료형의 이름 (객체의 포인터 or 참조형이 와야 함)
// expr : 변환의 대상 (변환이 적절한 경우에는 형 변화된 데이터를 반환하고, 적절하지 않은 경우에는 컴파일 에러 발생)
예를 들어, 다음과 같은 클래스가 정의되어 있다고 가정해보자.
class SimpleCar{ ... };
class BestFriend{ ... };
이 두 클래스는 서로 전혀 상관없는 클래스이다. (상속도 X)
그런데 이 연관성 없는 두 클래스를 대상으로, 아래와 같은 코드를 작성할 때 사용되는 것이 reinterpret_cast 연산자이다.
int main()
{
SimpleCar *car = new Car;
BestFriend * fren = reinterpret_cast<BestFriend*>(car);
// 포인터 변수 fren은 컴파일 환경에 따라 동작하는 방법이 달라짐. (따라서 어떤 방법으로 동작하는지 알지 못함)
...
}
이와 같이 reinterpret_cast 연산자는 포인터를 대상으로 하는, 그리고 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다.
포인터 변수 fren이 동작하는 방법은 우리가 알 수 없다. 따라서 reinpret_cast 연산자는 형 변환을 가능하게 하지만, 그에 대한 의미를 부여할 수는 없다는 것이다.
Case 5 )
그렇다면 이 연산자는 어떤 의미를 지닐까?
#include<iostream>
using namespace std;
int main() {
int num = 0x010203;
char* ptr = reinterpret_cast<char*>(&num);
// int형 정수에 바이트 단위 접근을 해서, int형 포인터를 char형 포인터로 변환하고 있음
for (int i = 0; i < sizeof(num); i++)
{
cout << static_cast<int>(*(ptr + i)) << endl;
// 바이트 단위 데이터를 문자가 아닌, 정수의 형태로 출력하며, char형 데이터를 int형으로 변환
}
return 0;
}
5. dynamic_cast 두 번째 이야기 : Polymorphic 클래스 기반의 형 변환
[ 정리 ] - 상속 관계에 놓여 있는 두 클래스 사이에서, 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 dynamic_cast 연산자를 활용한다. - 상속 관계에 놓여 있는 두 클래스 사이에서, 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 static_cast 연산자를 활용한다. |
그러나 dynamic_cast 연산자도, 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형으로의 형 변환을 허용한다. 기초 클래스가 Polymorphic 클래스라는 조건만 만족하면 말이다.
* Polymorphic 클래스 : 하나 이상의 가상함수를 지니는 클래스
Case 6 )
#include<iostream>
using namespace std;
class SoSimple { // Polymorphic 클래스
public:
virtual void ShowSimpleInfo() { // 가상함수
cout << "SoSimple Base Class" << endl;
}
};
class SoComplex :public SoSimple {
// SoSimple 클래스와 마찬가지로, SoComplex 클래스도 polymorphic 클래스
public:
void ShowSimpleInfo() { // 이 역시 가상함수
cout << "SoSimple Derived Class" << endl;
}
};
int main() {
SoSimple* simPtr = new SoComplex;
// 포인터 변수 simPtr이 실제 가르키는 것은, SoComplex 객체
SoComplex* comptr = dynamic_cast<SoComplex*>(simPtr);
// 기초 클래스인 SoSimple형 포인터 변수 simPtr을, 유도 클래스인 SoComplex 포인터 형으로 변환시키고 있으ㅡㅁ
// 그런데 기초 클래스인 SoSimple이 Polymorphic 클래스므로 dynamic_cast연산자로 형 변환 가능
comptr->ShowSimpleInfo();
return 0;
}
즉, 유도 클래스의 포인터 및 참조형으로의 형 변환을 시도할 때,
dynamic_cast
static_cast
두 연산자 모두 사용 가능한 것이다.
그러나 이 둘은 형 변환을 시도한다는 사실에는 차이가 없지만, 그 결과에는 큰 차이가 있다.
Case 7 )
#include<iostream>
using namespace std;
class SoSimple { // Polymorphic 클래스
public:
virtual void ShowSimpleInfo() { // 가상함수
cout << "SoSimple Base Class" << endl;
}
};
class SoComplex :public SoSimple {
// SoSimple 클래스와 마찬가지로, SoComplex 클래스도 polymorphic 클래스
public:
void ShowSimpleInfo() { // 이 역시 가상함수
cout << "SoSimple Derived Class" << endl;
}
};
int main() {
SoSimple* simPtr = new SoSimple;
SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);
// simPtr이 가리키는 대상(SoSimple)을, comPtr(SoComplex)이 가리킬 수 없는 상황
// 이러한 경우에는 형 변환의 결과로 NULL 포인터가 발생됨.
/* SoSimple* simPtr = new SoComplex;
SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);*/
// 이는 simPtr이 가리키는 객체를, SoComplex가 가리킬 수 있음
if (comPtr == NULL)
{
cout << "형 변환 실패" << endl;
}
else
{
comPtr->ShowSimpleInfo();
// 형 변환에 성공했ㅇ르 때만, 맴버함수를 호출하도록.
}
return 0;
}
이와 같이 dynamic_cast는 안정적인 형 변환을 보장한다.
특히, 컴파일 시간이 아닌 실행 시간(프로그램이 실행중인 동안)에 안전성을 검사하도록 컴파일러가 바이너리 코드를 생성한다는 점에 주목할 필요가 있다.
이로 인해 실행속도는 늦어지지만, 그만큼 안정적인 형 변환이 가능하다는 것이다.
이와 반대로 static_cast연산자는, 안전성을 보장하지 못한다. 컴파일러는 무조건 형변환이 되도록 바이너리 코드를 생성하기 때문에, 그로 인한 실행의 결과도 전적으로 프로그래머가 책임져야 한다.
물론 실행속도는 빠르지만, 안전성 검사를 별도로 진행이 불가능하다는 것이다.
6. dynamic_cast 세 번째 이야기 : bad_cast 예외
bad_cast 예외는 dynamic_cast 연산자를 이용한 형 변환의 과정에서 발생할 수 있는 예외임
Case 8 )
#include<iostream>
using namespace std;
class SoSimple {
public:
virtual void ShowSimpleInfo() {
cout << "SoSimple Base Class" << endl;
}
};
class SoComplex :public SoSimple {
public:
void ShowSimpleInfo() {
cout << "SoComplex Derived Class" << endl;
}
};
int main() {
SoSimple simObj;
SoSimple& ref = simObj;
try
{
SoComplex& comRef = dynamic_cast<SoComplex&>(ref);
// 참조자 ref가 실제 참조하는 대상이 SoSimple객체
// 따라서 SoComplex형으로의 참조형 변환은 안전하지 못함.
// 그러나 참조자를 대상으로는 NULL을 반환할 수 없기 때문에, 이러한 상황에서는 bad_cast 예외가 발생함
comRef.ShowSimpleInfo();
// 예외의 발생으로 실행되지 못함.
}
catch (bad_cast expt)
{
cout << expt.what() << endl;
// What() : 예외의 원인을 문자열로 반환해주는 함수 (컴파일러마다 반환하는 문자열의 내용은 다름)
}
return 0;
}
이 예제와 같이, 참조형을 대상으로 dynamic_cast 연산을 진행할 경우에는, bad_cast 예외가 발생할 수 있기 때문에 반드시 예외처리가 필요함
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |