멈추지 않고 끈질기게
[C++] 타입 변환 연산자(Type Casting Operator) 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
※ 해당 포스팅은 C++ 14 버전을 기준으로 작성되었습니다.
이번 포스팅에서는 C++의 타입 변환 연산자에 대해 알아보겠습니다.
1. static_cast<Type>
static_cast는 논리적으로 문제가 없는 형 변환에 사용하는 연산자입니다. 예를 들어 정수 <-> 소수 간 변환(int <-> float)이나 자식<->부모 클래스 간 형변환을 지원합니다.
class Animal
{
};
class Dog : public Animal
{
};
class Cat : public Animal
{
};
int main()
{
// float 변환 X
float ratio1 = 10 / 100;
cout << ratio1 << endl; // 0 출력
// float 변환 O
float ratio2 = static_cast<float>(10) / 100;
cout << ratio2 << endl; // 0.1 출력
// 업 캐스팅(암시적 변환 가능 가능)
Cat* c = new Cat();
Animal* a1 = static_cast<Cat*>(c);
// 다운 캐스팅(암시적 변환 불가)
Animal* a2 = new Dog();
Dog* dPtr = static_cast<Dog*>(a2);
}
상기 코드에서 float1의 경우 정수 / 정수 를 실행하므로 0이 되고, float2는 10을 static_cast를 통해 float 형으로 변환한 다음 나누므로 0.1이 됩니다. Cat, Dog 클래스는 모두 Animal 클래스를 상속받으며, Cat -> Animal(자식->부모) 형변환 및 Animal -> Dog(부모->자식) 형변환 모두 지원함을 알 수 있습니다.
다만 자식 -> 부모 형변환은 원래 암시적 변환도 지원하는 문제의 여지가 적은 변환이지만, 부모->자식 형변환은 암시적으로는 불가능한 다소 위험요소가 있는 변환입니다. static_cast는 대부분 변환해도 큰 문제가 없는 경우에 사용되지만, 부모->자식 형변환에 사용할 경우에는 주의가 필요합니다.
2. dynamic_cast<Type>
dynamic_cast는 클래스 간 변환에만 사용할 수 있는 타입 변환 연산자입니다.
class Animal
{
public:
virtual ~Animal() {} // dynamic_cast 조건
};
class Dog : public Animal
{
// Animal 상속 클래스
};
class Cat : public Animal
{
// Animal 상속 클래스
};
class Weapon
{
// Dog, Cat과 아무 연관이 없는 클래스
};
int main()
{
// 다운 캐스팅
Animal* a1 = new Dog();
Dog* d = dynamic_cast<Dog*>(a1);
// 업 캐스팅
Cat* c = new Cat();
Animal* a2 = dynamic_cast<Cat*>(c);
// 상속 관계 X
Animal* a3 = new Animal();
Weapon* w = dynamic_cast<Weapon*>(a3);
// nullptr 체크 로직
if (w == nullptr)
cout << "변환 결과가 nullptr 입니다." << endl;
}
상기 예제를 보면 static_cast와 비슷하게 자식->부모, 부모->자식을 모두 지원함을 알 수 있습니다. dynamic_cast의 경우 여기서 추가로 안정장치를 지원하는데, 바로 상속 관계가 없는 클래스 간 변환을 시도할 경우 널포인터(nullptr)를 반환한다는 점입니다. 예제의 마지막 부분처럼 전혀 연관이 없는 클래스로 변환을 시도할 경우 static_cast와 달리 컴파일은 성공하지만, 반환값이 널포인터라 널포인터 체크 로직에서 걸리는 부분을 볼 수 있습니다. 따라서 dynamic_cast를 사용할 때는 가급적 널포인터 체크 로직과 함께 사용하는 것이 좋습니다.
dynamic_cast를 사용하려면 피연산자의 클래스에 virtual 함수가 필요합니다. 위 예제 코드에서 Animal의 virtual로 선언한 소멸자를 지우면 사진과 같이 '피연산자는 다형 클래스 형식이어야 합니다' 라는 에러가 발생합니다. dynamic_cast로 형변환 시 클래스 간의 상속 관계가 있는지 파악해야 하고, 이를 위해 가상 함수 테이블(vftable)을 이용하기 때문입니다. 가상 함수 테이블은 클래스에 가상(virtual) 함수가 최소 1개 이상 있어야 메모리에 생성되기 때문에 이러한 조건이 붙는 것입니다. 또한 가상 함수 테이블을 통해 상속 관계를 파악하는 과정이 필요하기 때문에 결과적으로 static_cast에 비해서는 변환 속도가 다소 느릴 수 있습니다.
3. reinterpret_cast<Type>
reinterpret_cast는 형변환 연산자 중 가장 위험한 타입의 연산자입니다. interpret은 '해석하다'라는 뜻의 단어로, reinterpret_cast는 한마디로 (지정한 타입으로)재해석 하겠다는 뜻입니다.
int main()
{
Dog* d = new Dog();
int address = reinterpret_cast<int>(d);
cout << d << endl;
cout << address << endl;
}
상기 코드를 보면 Dog 클래스의 포인터를 reinterpret_cast를 통해 int형 변수에 대입하고 있습니다. 참고로 포인터 변수도 그대로 출력할 수 있지만 16진수로 출력되고, int형 변수에 넣어 출력하면 10진수로 출력함을 알 수 있습니다.
reinterpret_cast는 이처럼 아무 연관이 없는 타입 간의 변환도 허용해주는 강력한 변환 연산자입니다. static_cast나 dynamic_cast로 변환할 수 없는 경우 이 연산자를 사용해야 합니다. 다만 변환 가능한 타입에 아무 제약도 없으며 dynamic_cast같은 잘못된 변환을 검출할 수단을 제공하지도 않으므로, 사용 전 반드시 검토가 필요합니다. 특히나 C++은 포인터의 타입을 잘못 변환하여 사용하면 메모리 오류를 발생시킬 수도 있으므로, static_cast나 dynamic_cast로 해결할 수 있는 상황이라면 그쪽을 사용하는 것이 좋습니다.
4. const_cast<Type>
const_cast는 이름에서 알 수 있듯이 const 키워드를 떼고 붙이는데 사용하는 타입 변환 연산자입니다.
void Test(char* str)
{
cout << str << " - char* 타입" << endl;
}
void Test(const char* str)
{
cout << str << " - const char* 타입" << endl;
}
int main()
{
char c[] = { 't', 'e', 's', 't', '\0' };
Test(c); // char* 타입 전달
Test(const_cast<const char*>(c)); // const char* 타입 전달
cout << endl;
Test("const_cast 테스트"); // const char* 타입 전달
Test(const_cast<char*>("const_cast 테스트")); // char* 타입 전달
}
각각 char* 타입과 const char* 타입을 매개변수로 받아 출력한 후 실행된 함수의 종류를 출력해주는 Test() 함수를 선언하고, char 배열을 생성한 뒤 Test() 함수를 통해 출력했습니다. c를 그냥 넘겼을 때는 char* 타입 함수가, const_cast 함수로 변환하여 넘겼을 때는 const char* 타입 함수가 실행되었음을 알 수 있습니다.
또한 C++에서 " "를 통해 생성한 문자열의 경우 기본적으로 const char* 타입입니다. 따라서 char* 타입의 매개변수를 받는 함수에는 문자열을 그대로 매개변수로 사용할 수 없습니다. 상기 예제 코드의 경우 Test() 함수의 매개변수로 문자열을 그대로 전달한 경우에는 const char* 타입의 함수가 실행되고, const_cast로 변환하여 전달한 경우 char* 타입의 함수가 실행됨을 알 수 있습니다.
참고로 const_cast는 포인터나 참조(&) 변수에만 사용할 수 있는 연산자입니다. const int 와 같이 상수로 선언한 변수에 사용하여 일반 변수로 만드는 등의 변환은 불가능합니다(const로 선언한 일반 변수는 런타임 중에 변환할 수 없습니다).
5. 타입 변환 연산자를 사용해야 하는 이유
사실 reinterpret_cast의 기능까지 포함하는 강력한 타입 변환 방법이 있습니다. 그냥 변수 앞에 (Type)을 붙여주는 방식입니다.
class Dog
{
};
class Cat
{
};
int main()
{
Dog* d = new Dog();
Cat* c = (Cat*)d; // reinterpret_cast
}
상기 예제 코드의 경우 Dog와 Cat 클래스 간에는 어떠한 관계도 존재하지 않지만, 그냥 Dog* 포인터 변수 앞에 (Cat*)을 추가해주는 것만으로 변환할 수 있습니다. 그렇다면 왜 굳이 타입 변환 연산자들을 사용해야 할까요? 바로 부적절한 타입 변환을 막고 목적을 분명히 하기 위함입니다.
class Dog
{
virtual ~Dog() {} // dynamic_cast를 위한 가상 함수 선언
};
class Cat
{
};
int main()
{
Dog* d = new Dog();
// static_cast
// Cat* c1 = static_cast<Cat*>(d);
// dynamic_cast
Cat* c2 = dynamic_cast<Cat*>(d);
if (c2 == nullptr)
cout << "c2는 널포인터 입니다." << endl;
// retinterpret_cast
Cat* c3 = reinterpret_cast<Cat*>(d);
}
상기 예제에서 (Cat*)대신 static_cast를 사용할 경우 잘못된 형식 변환이라고 뜨며 컴파일 레벨에서 에러가 발생합니다(실행이 불가능하여 주석 처리했습니다). Dog 클래스에 virtual 함수가 있다면 dynamic_cast를 사용할 수 있지만, 결과값으로 널포인터를 반환합니다. 따라서 static_cast로 변환하려고 하면 컴파일 에러를 보고 논리적으로 변환 가능한 타입이 아니다(상속 관계가 없다)라는 것을 알 수 있고, dynamic_cast의 경우 nullptr 체크 과정에서 마찬가지로 변환 가능한 타입이 아니라는 것을 알 수 있습니다. 따라서 의도치 않은 강제적인 변환을 예방할 수 있고, 정말 의도한 변환이라면 마지막 라인처럼 reinterpret_cast를 통해 구현할 수 있습니다.
또한 코드를 읽는 다른 사람에게 의도를 전달하기 용이합니다. (Cat*)d 같은 코드를 다른 사람이 본다면 Dog 타입 포인터를 Cat타입 포인터로 변환하는 것을 보고 의아하게 생각할 수 있겠지만, reinterpret_cast를 사용했다면 분명한 의도를 가지고 강제로 형변환 하는 것임을 쉽게 알 수 있을 것입니다. 따라서 형변환 시 다소 귀찮더라도 (Type)과 같은 포괄적인 형변환 방법을 사용하는 대신, 상황에 맞는 타입 변환 연산자를 사용하여 실수를 예방하고 목적을 분명히 하는 것이 좋겠습니다.
'C++' 카테고리의 다른 글
[C++] std::endl vs "\n" (2) | 2024.07.23 |
---|---|
[C++] 람다 표현식(lambda expression) (0) | 2023.12.05 |
[C++] 스마트 포인터(Smart Pointer) (0) | 2023.11.23 |
[C++] 가상 함수(virtual function) (0) | 2023.03.22 |
[C++] 포인터(Pointer) (0) | 2023.03.08 |