개요

C++ 언어의 자료형 변환은 2가지로 나뉜다. 암묵적 자료형 변환명시적 자료형 변환이 바로 그것이다. 암묵적 자료형 변환이란 자료형이 자동으로 변환되는 것을 의미하고 명시적 자료형 변환이란 자료형을 강제로 변환하는 것을 의미한다.

C++에서는 다음과 같은 코드로 자료형을 확인할 수 있다.

 
typeid(표현식).name()
 

이러한 코드를 사용하려면 <typeinfo>라는 헤더 파일을 추가해야 한다.

암묵적 자료형 변환

서로 다른 자료형을 연산하면 C++ 컴파일러는 연산 전에 암묵적 자료형 변환 implicit type conversion을 수행한다. 암묵적 자료형 변환은 피연산자의 자료형을 적용할 수 있는 다른 자료형으로 자동으로 변환하는 것을 의미한다.

암묵적 자료형 변환은 암묵적 자료형 승격 implicit type promotion암묵적 자료형 변경 implicit type change이 있다.

암묵적 자료형 승격은 작은 자료형의 피연산자를 더 큰 자료형으로 승격하는 행위를 의미한다.

암묵적 자료형 변경은 이항 연산자를 사용할 때 두 자료형의 자료형을 맞추는 행위를 말한다.

변환은 하나만 일어날 수도 있고 2가지 모두 일어날 수도 있다.

암묵적 자료형 승격

암묵적 자료형 승격은 산술 연산자의 피연산자에 자동으로 적용된다. 암묵적 자료형 승격은 2가지 경우에 이루어진다.

첫 번째, 피연산자의 자료형이 산술 연산에 적합하지 않은 경우(불 또는 문자)

두 번째, 피연산자의 자료형에 맞는 연산자가 없는 경우(short 또는 float)

규칙원래 자료형승격된 자료형
1boolint
2charint
3shortint
4unsigned shortunsigned int
5floatdouble

암묵적 자료형 변경

암묵적 자료형 승격이 일어난 이후, 컴파일러는 필요한 경우 암묵적 자료형 변경을 수행한다. 암묵적 자료형 변경은 두 피연산자의 자료형이 다를 때 발생한다. 단항 연산의 경우, 암묵적 자료형 변경이 일어나지 않는다. 이항 연산의 경우, 피연산자를 동일하게 만들기 위해서 암묵적 자료형 변경이 필요할 수 있다. 2가지 상황을 살펴보자

부가 작용이 없는 표현식

부가 작용이 없는 표현식의 경우, 아래와 같이 자료형을 상위 계층으로 변환한다.


int -> unsigned int -> long -> unsigned long -> double -> long double

이전에 살펴보았던 암묵적 자료형 승격이 이미 일어난 경우는 계층이 변환된 상태이므로 암묵적 자료형 변경이 일어나지 않는다.

부가 작용이 있는 표현식

변수에 값을 할당할 때, 소스 source와 대상 destination의 자료형이 다른 경우에도 자료형 변환이 발생한다. 이때 컴파일러는 소스의 자료형을 대상의 자료형에 맞게 변경한다. 소스의 자료형이 대상의 자료형보다 크기가 큰 경우 소스의 자료형을 강등시키고, 그 반대의 경우 소스의 자료형을 승격시킨다.

명시적 자료형 변환

c++에서는 c 스타일의 형 변환 연산자를 가리켜 ‘오래된 C 스타일 형 변환 연산자(Old C-style cast operator)‘라 부르기도 한다. 이렇듯 c 스타일의 형 변환 연산자는 c 언어와의 호환성을 위해서 존재할 뿐, c++에서는 새로운 형 변환 연산자와 규칙을 제공하고 있다.

c 스타일의 형 변환 연산자는 그 기능이 강력해서, 아래와 같이 형 변환이 일어나면 안 되는 케이스에서도 형변환이 일어날 수 있다.

 
class Car
{
	//생략
};
 
class Truck : public Car
{
	//생략
};
 
int main(void)
{
	Car *pcar1 = new Truck(80, 200);
	Truck *ptruck1 = (Truck *)pcar1; //문제 없어 보이는 형 변환
	
	Car *pcar2 = new Car(120);
	Truck *ptruck2 = (Truck *)pcar2; //문제가 바로 보이는 형 변환
}
 

기초 클래스의 포인터 형을 유도 클래스의 포인터 형으로 형 변환하는 것은 일반적인 경우의 형 변환이 아님에도 불구하고, 형 변환 연산자의 기능이 너무 강력하여 형 변환이 이루어지는 모습이다.

이러한 문제점 때문에 c++ 에서는 다음과 같이 4개의 연산자를 추가로 제공하면서 용도에 맞는 형 변환 연산자의 사용을 유도하고 있다.

  • dynamic_cast
  • static_cast
  • const_cast
  • reinterpret_cast

dynamic_cast : 상속관계에서의 안전한 형 변환

dynamic_cast 형 변환 연산자는 다음의 형태를 갖는다.

 
dynamic_cast<T>(expr)
 

dynamic_cast는 다음의 조건을 충족하여야 정상적으로 컴파일되며, 그렇지 않으면 컴파일 에러가 발생한다.

  1. <> 사이에 변환하고자 하는 자료형의 이름을 둔다. 자료형은 객체의 포인터 또는 참조형이여야 한다.
  2. () 사이에는 변환의 대상이 와야 한다.
  3. 상속 관계에 놓여 있는 두 클래스 사이에서 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환하는 경우여야 한다.

종합적으로 dynamic_cast는 다음을 의미한다.

상속 관계에 있는 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환한다.

 
/*DynamicCasting.cpp*/
 
#include <iostream>
using namespace std;
 
class Car
{
	// 생략
};
 
class Truck
{
	// 생략
};
 
int main(void)
{
	Car *pcar1 = new Truck(80, 200);
	//Truck *ptruck1 = dynamic_cast<Truck *>(pcar1); -> 에러, 그러나 경우에 따라선 필요한 형 변환이므로 그러한 경우 static_cast를 사용
 
	Car *pcar2 = new Car(120);
	//Truck *ptruck2 = dynamic_cast<Truck *>(pcar2); -> 에러
 
	Truck *ptruck3 = new Truck(70, 150);
	Car *pcar3 = dynamic_cast<Car *>(ptruck3); //컴파일 OK!
}
 

그러나 다음의 예외적인 경우에, dynamic_cast는 부모 자식으로의 형변환을 허용한다.

부모 클래스가 ‘Polymorphic 클래스’인 경우

 
/*PolymorphicDynamicCasting.cpp*/
#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(void)
{
	SoSimple *simPtr = new SoComplex;
	SoComplex *comPtr = dynamic_cast<SoComplex *>(simPtr);
	comPtr->ShowSimpleInfo();
	
	return (0);
}
 
  • static_cast와 dynamic_cast의 차이점

위 예제에서 simPtr은 SoComplex 객체를 가리키고 있다. 부모자식으로 dynamic_cast가 성립하려면 포인터가 가리키고 있는 객체의 자료형이 변환 후 자료형과 일치해야 한다. 그렇지 않으면 형 변환의 결과로 NULL이 반환된다.

이렇듯 dynamic_cast는 안정적인 형 변환을 보장한다. 특히 다음의 사실에 주목할 필요가 있다.

컴파일 시간이 아닌 실행 시간에(프로그램이 실행 중인 동안에) 안정성을 검사하도록 컴파일러가 바이너리 코드를 생성한다

이렇게 ‘동적’으로 안정성 검사 & casting이 이루어지는 특성 때문에 dynamic이라는 명칭이 붙었다.

반면에 static_cast는 이러한 안정성 검사가 이루어지지 않는다. 컴파일러는 무조건 형 변환이 되도록 바이너리 코드를 생성한다. 이러한 특성 때문에 ‘static’이라는 명칭이 붙었다.

dynamic_cast를 이용한 형 변환 중 예외가 발생할 경우 bad cast 예외가 발생한다.


static_cast: A 타입에서 B 타입으로

 
static_cast<T>(expr)
 

dynamic_cast가 자식 부모로의 형 변환만 허용하는데 반하여 static_cast는 보다 넓은 범위의 형변환을 허용한다.

  1. 자식 부모로의 형 변환뿐만이 아닌, 부모 자식으로의 형변환 또한 허용한다.
  2. 기본 자료형(여기서 기본 자료형이라함은 포인터 자료형을 제외한 것을 말한다) 데이터 간의 형 변환에도 사용이 된다.

따라서 부모 자식으로의 캐스팅 같은 잘못된 형 변환을 할 수 있으므로 이에 대하여 개발자는 보다 신중하게 사용하여야 한다.

 
/*StaticCasting.cpp*/
 
#include <iostream>
using namespace std;
 
class Car
{
	// 생략
};
 
class Truck
{
	// 생략
};
 
int main(void)
{
	Car *pcar1 = new Truck(80, 200);
	Truck *ptruck1 = static_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!
}
 
  • static_cast가 dynamic_cast 보다 빠르기 때문에 dynamic_cast를 사용해도 되는 상황에 static_cast를 사용하는 경우도 있다.

const_static : const 성향 삭제

C++에서는 포인터와 참조자의 const 성향을 제거하는 형 변환을 목적으로, 다음의 형 변환 연산자를 제공한다.

 
const_cast<T>(expr)
 
 
/*ConstCasting.cpp*/
 
#include <iostream>
using namespace std;
 
void ShowString(char *str)
{
	cout<<str<<endl;
}
 
void ShowAddResult(int &n1, int &n2)
{
	cout<<(n1 + n2)<<endl;
}
 
int main(void)
{
	const char *name = "Lee Sung Ju";
	ShowString(const_cast<char *>(name));
	
	const int &num1 = 100;
	const int &num2 = 200;
	ShowAddResult(const_cast<int &>(num1), const_cast<int &>(num2));
	
	return (0);
}
 

위 예제에서와 같이 const_cast 형 변환 연산은, 함수의 인자전달 시 const 선언으로 인한 형(type)의 불일치가 발생해서 인자의 전달이 불가능한 경우에 유용하게 사용이 된다.

  • const_cast 연산자는 volatile 특성을 제거하는 데에도 사용할 수 있다.

reinterpret_cast: 상관없는 자료형으로의 형 변환

reinterpret_cast 연산자는 전혀 상관이 없는 자료형으로의 형 변환에 사용이 되며, 기본적인 형태는 다음과 같다(마찬가지로 앞서 설명한 형 변환 연산자들과 동일하다).

 
reinterpret_cast<T>(expr)
 

부연하자면, reinterpret_cast 연산자는 포인터를 대상으로 하는, 그리고 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다.

reinterpret_cast는 다음과 같은 형변환도 가능할 정도로 강력하다.

 
class SimpleCar { /*. . . .*/ };
class BestFriend { /*. . . ./*/ };
 
int main(void)
{
	SimpleCar *car = new Car;
	BestFriend *fren = reinterpret_cast<BestFriend *>(car); //상속 관계도, 그 외에 아무 관계도 없는 자료형 간의 형변환이 가능할 정도로 강력하다.
}
 

물론 위 예제는 전혀 실용적이지 않다. reinterpret_cast는 일반적으로 다음과 같은 상황에 쓰인다.

 
#include <iostream>
using namespace std;
 
int main(void)
{
	/*4 바이트 자료형인 int를 1 바이트 단위로 출력하고자 할 때*/
	int num = 0x010203;
	char *ptr = reinterpret_cast<char *>(&num);
	
	for (int i = 0; i < sizeof(num); i++)
		cout<<static_cast<int>(*(ptr + i))<<endl;
	
	return (0);
}
 
 
int main(void)
{
	int num = 72;
	int *ptr = &num;
 
	int adr = reinterpret_cast<int>(ptr); //주소 값을 정수로 변환
	cout<<"Addr: "<<adr<<endl; //주소 값 출력
 
	int *rptr = reinterpret_cast<int *>(adr); //정수를 다시 주소 값으로 변환
	cout<<"value: "<<*rptr<<endl; //주소 값에 저장된 정수 출력
}
 

참고자료

윤성우의_열혈_cpp_프로그래밍