C++의 캐스트 연산자
캐스트 연산자의 한계
캐스트 연산자는 변수의 타입을 원하는 대로 바꿀 수 있어 편리하다.
타입은 가급적 맞추어 쓰는 것이 바람직하지만 불가피하게 타입을 바꿔야하는 상황이 있고 void * 처럼 캐스트 연산자가 꼭 필요한 경우도 있다.
그러나 지시를 너무 잘 따른다는 면에서 부작용이 있고 실수의 가능성도 높다.
#include <stdio.h>
int main()
{
const char* str = "korea";
int* pi;
pi = (int*)str;
printf("%d %x \n", *pi, *pi);
}
문자열 포인터 str이 가리키는 번지를 정수형 포인터(int *)로 강제 캐스팅해서 pi에 대입한 후 pi가 가리키는 곳의 내용을 읽었다.
* 연산자는 포인터의 타입에 따라 읽을 길이와 비트 해석 방법을 결정한다.
pi가 가리키는 곳에 문자열이 들어 있지만 pi가 정수형 포인터이므로 정수로 읽는다.
16진수로 출력한 값은 "korea"문자값을 반대로 나열한 것임을 어렴풋이 짐작할 수 있다.
그러나 10진수로 읽은 17억이라는 값은 실제 저장된 "korea"문자열과 pi로 읽은 17억은 논리적 연관이 없으며 쓸모없는 값이다.
캐스트 연산자는 이런 의미없는 변환까지도 허용해버려 실수했을때 엉뚱한 결과가 나오도록 방치한다.
그나마 이 경우는 할당은 되어있는 곳이므로 안정성에 문제는 없다.
int value = 12345678;
char *str = (char *)value;
puts(str);
정수값을 char*로 캐스팅하여 이 번지의 문자열을 읽었다.
이 번지가 다행히 읽을 수 있는 곳이라면 쓰레기 문자열이라도 나오겠지만 허가되지 않은 영역이라면 프로그램이 다운되어 버린다.
메모리는 운영체제가 알아서 관리하는데 절대 번지를 마음대로 액세스 하는 것은 실용성도 없고 위험하다.
C의 캐스트 연산자는 컴파일러는 문제가있다고 인식되지만 명시적으로 캐스팅함으로써 개발자가 책임을 지는 방법이다.
C++은 기능을 축소하고 엄격한 규칙을 적용한 4개의 새로운 캐스트 연산자를 도입했다.
어떤 변환인지 의도를 분명히 밝히도록해서 의도치 않은 실수를 줄이도록 만들었다.
static_cast
static_cast 연산자는 논리적으로 변환 가능한 타입만 변환하며 그 외의 변환은 에러로 처리한다.
사용 방법이 다소 생소한데 나머지 캐스트 연산자도 형식은 비슷하다.
static_cast<타입>(대상)
static_cast는 키워드이며 <> 괄호 안에 변환할 타입을 적고 () 괄호 안에 캐스팅할 대상을 적는다.
// static_cast.cpp
#include <stdio.h>
int main()
{
const char* str = "korea";
int* pi;
double d = 123.456;
int i;
int ary[] = { 1, 2, 3, 4, 5 };
char aary[] = { 'a', 'b' };
int* ptr = static_cast<int*>(ary); // 배열은 int형으로 바꿀 수 있다.
//ptr = static_cast<int*>(aary); // 포인터끼리의 형변환 => 에러
i = static_cast<int>(d); // 가능
//pi = static_cast<int*>(str); // 에러 : static_cast는 포인터끼리 형변환은 허용되지않음.
pi = (int*)str; // 가능
}
실수형을 정수형으로 캐스팅하거나 반대로 캐스팅하는 것은 허용된다.
두 타입은 수치형이라는 점에서 공통적이고 약간의 정밀도 희생은 있지만 호환가능한 타입이다.
열거형과 정수형 간의 변환이나 double과 float 형 간의 변환도 허용된다.
그러나 포인터의 타입을 다른 것으로 변경하는 것은 금지된다.
문자형 포인터 str을 정수형 포인터로 캐스팅하면 컴파일 에러로 처리하여 실수를 방지한다.
논리적으로 이런 변환이 필요한 경우가 드물어 의도한 것이라기보다 실수일 가능성이 높기 때문이다.
상속 관계에 있는 포인터끼리는 상호 연관성이 있어 static_cast 연산자로 변환할 수 있다.
자식 객체의 포인터가 부모 객체 타입으로 바뀔 수 있고 부모 타입으로 받았다가 자식 타입으로 바꿀 수도 있다.
그 외의 경우는 변환을 거부하고 컴파일 에러로 처리한다.
// static_cast2
#include <stdio.h>
class Parent {};
class Child : public Parent {};
int main()
{
Parent P, *pP;
Child C, *pC;
int i = 1234;
pP = static_cast<Parent*>(&C); // 가능
pC = static_cast<Child*>(&P); // 가능은하지만 위험
//pP = static_cast<Parent*>(&i); // 에러 => 하극상 형태가되면 안됨..
//pC = static_cast<Child*>(&i); // 에러
}
계층을 이루는 두 개의 클래스를 선언하고 4종류의 변환을 해보았다.
첫번째는 자식 객체를 부모 타입으로 바꾸는데 이것은 언제나 가능하다.
상속 계층의 위쪽으로 변환하는 업캐스팅(Upcasting)은 항상 안전하며 따라서 캐스팅할 필요도 없다.
두번째는 부모 객체의 번지를 자식 타입의 포인터로 바꾸는 다운 캐스팅(DownCasting)을 하는데 항상 안전하지는 않아 캐스트 연산자를 사용해야한다.
부모 객체가 자식 타입의 모든 멤버를 가지고있지 않아 위험하지만 static_cast는 실행 중 타입 체크까지는 하지 않으므로 이 변환의 위험성을 판단할 수 없다.
뒤쪽 두 개의 변환은 정수형 포인터를 아무런 관련이 없는 클래스형 포인터로 변환하므로 금지된다.
정수형 변수가 Parent나 Child 멤버를 가지고 있지 않으니 변환할 필요조차 없다.
의미없는 변환이라 판단하여 에러 처리한다.
# static_cast 정리
1. Compile 타임에 형변환에 대한 오류를 검사한다.
2. 논리적으로 변환 가능한 타입일 경우 변환 가능
3. 포인터의 타입을 다른 것으로 변경하는 것은 변환 불가능 (Compile Error)
4. 상속 관계에 있는 경우 변환이 가능하다
5. 상속 관계의 변환 시 업캐스팅은 항상 안전하며, 다운 캐스팅의 경우 안전하지 않다.
(부모가 자식의 모든 멤버를 가지고 있지 않기 때문)
dynamic_cast
다운 캐스팅은 대상 변수가 실제 어떤 객체를 가리키는가에 따라 안전성 여부가 결정된다.
다운 캐스팅이 안전하려면 부모 타입의 포인터가 진짜 자식 객체를 가리키고 있어야한다.
이를 정확하게 판단하려면 실행 중 타입 정보를 참조해야하는데 dynamic_cast는 RTTI 정보를 참조하여 안전한 변환만 허용한다.
안전한 변환이 아닌 경우 NULL을 리턴하여 위험한 변환을 허가하지 않는다.
실행 중 타입 정보를 사용하므로 이 연산자를 사용하려면 RTTI 기능이 켜져있어야 한다.
// 파일이름 : dynamic_cast.cpp
#include <stdio.h>
#include <typeinfo>
class Parent
{
public:
virtual void PrintMe() { printf("I am Parent\n"); }
};
class Child : public Parent
{
private:
int num;
public:
Child(int anum) : num(anum) {}
virtual void PrintMe() { printf("I am Child\n"); }
void PrintNum() { printf("Hello Child = %d\n", num); }
};
void func(Parent* p)
{
p->PrintMe();
Child* c = dynamic_cast<Child*>(p);
if (c)
{
c->PrintNum();
}
else
{
puts("이 객체는 num을가지고 있지 않습니다.");
}
}
int main()
{
Parent p;
Child c(5);
func(&c);
func(&p);
}
인수로 전달된 포인터 p를 Child *로 동적캐스팅 한다.
이때 p가 가리키는 객체가 Child 타입이면 제대로 변환되며, 그렇지 않으면 NULL이 리턴된다.
dynamic_cast가 무사히 캐스팅했다면 p의 대상체는 분명 Child 객체이며 따라서 이 객체로부터 PrintNum을 호출해도 안전하다.
실행 중 타입 점검과 캐스팅까지 한꺼번에 할 수 있어 typeid 연산자보다 간편하다.
dynamic_cast는 상속관계의 포인터끼리 변환할때 사용하는데 레퍼런스에 대해서도 사용할 수 있다.
다만 레퍼런스는 NULL이 없으므로 캐스팅할 수 없을때 bad_cast 예외를 던진다.
레퍼런스에 대해 이 캐스팅을 적용할때는 코드를 try 블록에 작성하고 bad_cast 예외를 처리해야한다.
# dynamic_cast 정리
1. 런타임에 동작하는 객체의 포인터를 위한 Casting으로, RTTI 정보를 참조하여 타입을 변환한다. 따라서 가상함수 테이블이 존재해야 한다.
2. 타입이 정상적으로 변환되지 않으면 NULL이 return된다.
3. 레퍼런스로 변환을 시도 중 문제 발생 시 bad_cast 예외가 발생한다.
4. 주로 안전한 Down Casting을 위해 사용되어 타입 변환 성공여부에 따라 로직을 처리하는 데에 사용된다. (Up, Cross Casting이 안되는 것은 아니다.)
#정적 캐스트와 동적 캐스트의 차이점
static_cast 는 정적 실행되기전에 컴파일러가 타입을미리 결정해서 반환
dynamic_cast는 프로그램 실행중에 타입이 결정되며 가상함수가 있는 클래스안에서 동작된다.
const_cast
const_cast 연산자는 포인터의 상수성만 변경한다.
상수 지시 포인터를 비상수 지시 포인터로 잠시 바꾸고 싶을때 이 연산자를 사용한다.
반대의 경우도 사용할 수 있지만 바로 대입 가능해 굳이 캐스팅할 필요는 없다.
// const_cast.cpp
#include <stdio.h>
int main()
{
char str[] = "string";
const char* c1 = str;
char* c2;
c2 = const_cast<char*>(c1);
c2[0] = 'a';
puts(c2);
}
상수 지시 포인터 c1에 str 문자 배열을 대입했다.
str 자체는 변경 가능하지만 c1이 상수만 가리키는 포인터여서 대상체를 변경할 수 없다.
c2는 상수가 아닌 일반 포인터이므로 c2 = c1으로 c1의 번지를 대입할 수 없다.
이 대입을 허락하면 c2로 상수 문자열을 부주의하게 바꿀 위험이 있다.
c1은 상수지만 대상체는 상수가 아니라는 것을 확실히 알고 있다면 char *로 캐스팅하여 상수성을 없앨 수 있으며 그 결과를 c2에 대입하면 c2로 대상체를 변경할 수 있다.
str은 배열이므로 상수가 아니며 따라서 c2가 이 번지를 가져도 이상 없다.
만약 str이 배열이 아닌 문자열 리터럴을 가리키는 상수라면 c2가 대입받아서는 안된다.
const_cast에 의해 const char* 타입이 char * 포인터 타입으로 잠시 바뀐다.
이 연산자는 포인터의 상수성만 바꿀 뿐이며 대상체 타입을 바꾼다거나 기본 타입을 포인터 타입으로 바꿀 수는 없다.
C++ 캐스트 연산자의 용도
static_cast : 상속 관계의 클래스 포인터 및 레서런스, 기본 타입, 타입 체크 안함
dynamic_cast : 상속 관계의 클래스 포인터 및 레퍼런스, 타입 체크, RTTI 기능 필요
const_cast : const, volatile 등의 속성 변경
reinterpret_cast : 포인터끼리, 포인터와 수치형 간의 변환(생략)
변환하고자하는 목적에 맞게 연산자를 잘 골라 사용하며 잘못 사용하면 컴파일중에 에러처리가 된다.
꼭 필요한 변환만 최소한으로 수행하므로 부주의한 캐스팅을 방지하고 실수를 금방 알 수 있게 해준다.
특이한 모양으로 인해 캐스트 연산자인지 금방 알아 볼 수 있어 가독성에도 유리하다.
C는 타입을 맞춰서 써야하지만 불가피하게 강제로 타입을 바꿔야하는 경우도있다.
하지만 캐스팅을 너무 남발하면 컴파일러의 타입 체크 기능을 방해하여 안정성을 떨어뜨리고 실수의 가능성이 높아진다. 그래서 기능을 제한하는 캐스트 연산자를 새로 도입한 것이다.
C++의 캐스트 연산자는 C에 비해 훨씬 안전하지만 그래도 위험한건 마찬가지라 주의 깊게 사용해야한다.
'C++ & C#' 카테고리의 다른 글
[C++] 실시간 타입 정보 (RTTI, Type_info) (0) | 2024.01.25 |
---|---|
[C++] 스마트 포인터 (0) | 2022.03.07 |
[C#] 가비지 컬렉터 (0) | 2021.11.22 |
[C++/STL] algorithm헤더의 sort함수 (0) | 2021.11.09 |
[C++] Pointer에 관하여 (0) | 2021.10.26 |