이번 포스팅에서는 캐스팅에 대한 내용을 정리해 보려고 한다.
캐스팅에는 암시적 캐스팅과 명시적 캐스팅이 있다. 암시적(Implicit) 캐스팅은 컴파일러가 형을 변환해 주는 것을 말한다. 단, 형 변환이 허용이 되고 프로그래머가 명시적으로 형 변환을 안 할 경우에 해당된다.
int number1 = 3;
long number2 = number1; // 암시적 캐스팅
명시적(Explicit) 캐스팅은 프로그래머가 형 변환을 위한 코드를 직접 작성하는 것을 의미한다. C++의 캐스팅은 다음과 같은 것들이 있다. 기존의 C 스타일 캐스팅은 아래의 4가지 캐스팅 중 하나를 했었고 컴파일러가 명확하게 잡지 못한다는 문제가 있어서 아래처럼 세분화 하게 되었다.
- static_cast
- const_cast
- dynamic_cast (C++98, 모던 C++)
- reinterpret_cast
// 정적 캐스팅(static casting)
float num1 = 3.f;
int num2 = static_cast<int>(num1);
Animal* myPet = new Cat(2, "Owen");
Cat* myCat = static_cast<Cat*>(myPet);
// 리인터프리트 캐스팅(reinterpret casting)
Animal* myPet = new Cat(2, "Owen");
unsigned int myPetAddr = reinterpret_cast<unsigned int>(myPet);
cout << "address: " << hex << myPet;
// 컨스트 캐스팅(const casting)
void Foo(const Animal* ptr)
{
// BAD CODE
Animal* animal = const_cast<Animal*>(ptr);
animal->SetAge(5);
}
// 동적 캐스팅(dynamic casting)
Cat* myCat = dynamic_cast<Cat*>(myPet);
static_cast
정적 캐스트는 값에 쓰일 수도, 개체에 쓰일 수도 있다. 값에 쓰일 경우 두 숫자 형 간의 변환이며 값을 유지한다. (반올림 오차는 제외) 하지만 이진수 표기는 달라질 수도 있다.
float num1 = 3.f;
int num2 = static_cast<int>(num1);
위의 예제를 보면 floating point 3.f를 int형 3으로 형변환을 했을 때 우리가 보는 값은 그대로 유지가 되지만 이진수는 바뀌는 것을 알 수가 있다.
개체 포인터에 쓰이는 경우는 변수형 체크 후 베이스 클래스를 파생 클래스로 변환한다. 컴파일 시에만 형 체크가 가능하고, 실행 도중 크래시가 날 가능성이 있다. 정적 캐스트에 대한 사용법은 아래와 같다.
reinterpret_cast
C++에서 가장 위험한 캐스팅 중 하나이다. 이 캐스팅이 위험한 이유는 다음과 같다.
- 전혀 연관이 없는 두 포인터 형 사이의 변환을 허용한다. (ex. Cat* <-> House*, char* <-> int*)
- 포인터와 포인터 아닌 변수 사이의 형 변환을 허용한다. (ex. Cat* <-> unsigned int)
정적 캐스팅과 다른 점은, 리인터프리터 캐스트는 이진수 표기가 달라지지 않는다는 점이다.
메모리 관점에서 보면 다음과 같다.
int* signedNumber = new int(-10);
unsigned int* unsignedNumber = reinterpret_cast<unsigned int*>(signedNumber);
-10이라는 int형 값을 힙 메모리 안에 저장하면 이진수로는 1111 1111 1111 1111 1111 1111 1111 0110이 된다. 이 이진수 값이 바뀌지 않으면서 형변환이 일어나게 되기 때문에 unsigned int는 2^32 - 10의 값을 가지게 된다. 우리가 보는 값은 바뀌게 되는 것이다.
const_cast
const_cast는 형을 바꿀 수는 없다. 오직 const나 volatile 어트리뷰트를 제거할 때 사용한다.
Animal* myPet = new Cat(2, "Owen");
const Animal* petPtr = myPet;
Animal* myAnimal1 = (Animal*)petPtr; // OK
Cat* myCat1 = (Cat*)petPtr; // OK
Animal* myAnimal2 = const_cast<Animal*>(petPtr); // OK
Cat* myCat2 = const_cast<Cat*>(petPtr); // Compile Error, static_cast를 사용해야 함
const_cast는 포인터 형에서만 사용이 가능하다. 값 형은 언제나 복사가 되기 때문이다. 그리고 const_cast를 코드에서 쓰려고 한다면 이는 문제가 있는 것이다. 이 캐스팅을 쓸 때는 서드파티 라이브러리에서 const를 제대로 사용하지 않았을 때와 같은 경우만 사용하는 것이 바람직하다.
dynamic_cast
동적 캐스트는 실행 중에 형을 판단한다. 그리고 포인터나 참조 형을 캐스팅 할 때만 쓸 수 있다. 또한 호환되지 않는 자식형으로 캐스팅을 하려 하면 NULL을 반환한다. 따라서 dynamic_cast가 static_cast보다 안전하다.
하지만 이 캐스팅을 쓰려면 컴파일 중에 RTTI(실시간 타입정보, Real-Time Type Information)를 켜야 한다. 참고로 이 옵션은 C++ 프로젝트에서는 성능적인 이슈로 끄는 경우가 일반적이다. RTTI가 꺼져 있으면 dynamic_cast와 static_cast가 동일하게 동작한다.
캐스팅에 대해서는 다음과 같은 규칙을 가지고 가는 것을 권장한다. 이 규칙은 제일 안전한 것부터 사용하기 시작해서 마지막에 가장 위험한 것을 쓰는 순서이다.
- 기본적으로 static_cast를 쓸 것
- reinterpret_cast를 쓸 것 (서로 연관이 없는 포인터 사이의 변환은 그 데이터형이 맞다고 정말 확신할 때만 쓸 것)
- 내가 변경권한이 없는 외부 라이브러리를 호출할 때만 const_cast를 쓸 것
참고자료
- C++ 언매니지드 프로그래밍, 포큐 아카데미
'Prog. Langs & Tools > C++' 카테고리의 다른 글
[C++] Ch09. 예외(Exception) (0) | 2021.01.11 |
---|---|
[C++] Ch08. 인라인 함수, static 키워드 (0) | 2020.11.30 |
[C++] Ch06. OOP 3 (0) | 2020.10.19 |
[C++] Ch05. OOP 2 (0) | 2020.10.01 |
[C++] Ch04. OOP 1 (0) | 2020.09.22 |