본문 바로가기

문과 코린이의, [C. C++] 기록/C++ 이론

[문과 코린이의 IT 기록장] C++ - C++에서의 형 변환 연산 (C에서의 형 변환 연산의 강력함, static_cast, const_cast, dynamic_cast, reinterpret_cast)

반응형

[문과 코린이의 IT 기록장] C++ - C++에서의 형 변환 연산 (C에서의 형 변환 연산의 강력함, static_cast, const_cast, dynamic_cast, reinterpret_cast)

[문과 코린이의 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*)&num; // 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 예외가 발생할 수 있기 때문에 반드시 예외처리가 필요

 

 


* 유의사항
- 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다.
- 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다.
- 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :)
반응형