본문 바로가기
C,C++/C++

[C++] 캐스팅 연산자에 대해 알아보자 | static_cast, reinterpret_cast

by woohyeon 2020. 1. 23.
반응형

static_cast와 reinterpret_cast는 C 타입의 캐스팅이 용도에 따라 분리되어 C++에 등장한 캐스팅 방법이다.

예를 들어 C에서는 다음과 같이 캐스팅을 하였고 웬만하면 모든 캐스팅이 가능했다.

unsigned int num = 20;
int signedNum = (int)num;


하지만 이는 사용자의 의도와 상관없이 대부분의 캐스팅을 허용해주기 때문에 사용자의 실수를 허용하는 일이 많았다.
그래서 C++에서 용도에 따라 사용하도록 4가지 캐스팅으로 나누어서 도입하였고 여기선 제일 자주 사용되는 2가지만 정리한다.
그 2가지는 static_cast와 reinterpret_cast이며 특징은 다음과 같다.


- 사용 방법

static_cast<변환할 타입>(변환할 대상)

 

reinterpret_cast<변환할 타입>(변환할 대상)



- 간단한 특징
1. static_cast는 크게 보면  포인터에 대해 캐스팅을 한다.
- static_cast는 이름 그대로 정적 캐스팅이다. 따라서 컴파일 타임에 캐스팅이 완료된다. 이 말은 런타임에 결정되는 타입은 반영이 안될 수 있다는 말이다.

2. reinterpret_cast는 포인터에 대한 캐스팅만 한다.
- reinterpret_cast는 값 형식에 대한 캐스팅을 제외하면 관계 없는 타입끼리도 캐스팅이 되기에 위험성이 높은 캐스팅이다.


◆ 값에 대한 캐스팅

- static_cast는 현재 값을 유지하려고 한다. 하지만 바이너리가 변경될 수 있다.
예를 들어 float형 변수 5.3f를 int형으로 캐스팅 할 때 소숫점을 버리고 정수 5가 된다. 이 것은 우리가 보기엔 값을 유지한다는 느낌이 든다. 하지만 부동소수점과 정수형은 바이너리 구조가 다르다. 정수 바이너리 0001이 부동소수점으로도 1.0인것은 아니다. 따라서 우리에게 값이 유지된다는 것을 보여주기 위해 필요하다면 바이너리를 변경한다. 

요약: 우리가 보는 관점에선 값의 변형이 적지만 컴퓨터가 보는 관점에선 클 수 있다. 

   ex) float(3.f):  0100 0000 0100 0000 -> int(3): 0000 0000 0000 0011

- reinterpret_cast는 값 형식의 캐스팅에 사용할 수 없다.


◆ 포인터에 대한 캐스팅

- static_cast는 오직 상속 관계의 포인터끼리만 캐스팅을 허용한다. static_cast는 이름 그대로 정적 캐스팅이기 때문에 컴파일 시간에 캐스팅이 완료되어야 한다. 따라서 a와 b가 상속 관계일 때라면 컴파일 에러없이 캐스팅이 될 수 있다. (이것은 위험한 상황을 만들 수 있으며 잠시 후 살펴 볼 예정)

- reinterpret_cast는 상속 관계 뿐만 아니라 모든 포인터 타입간의 형변환을 허용한다. 심지어 포인터 타입을 포인터가 아닌 타입으로도 캐스팅이 가능하며 그 반대도 가능하다.

- reinterpret_cast는 이전 값에 대한 바이너리를 유지한다. 다만 타입에 따라 출력되는 값이 다를 수 있다.
static_cast에서 말했듯이 float 타입과 int타입의 값이 동일하다해서 바이너리도 동일한 것은 아니라고 했다.
static_cast는 값을 유지하는 대신 바이너리를 변경했다면, reinterpret_cast는 값을 변경하는 대신 바이너리를 유지한다.

- reinterpret_cast는 캐스팅 후에도 바이너리가 유지되기 때문에 출력되는 값이 달라질 뿐 다시 캐스팅을 한다면 값이 복원된다.
- 따라서 포인터를 unsigned int 타입의 변수에 캐스팅하여 저장 후 다시 포인터 타입으로 캐스팅하여도 주소가 유지된다.

아래 코드는 int*의 값을 unsigned int 타입으로 변경 후 다시 int* 타입으로 캐스팅하였을 때 값이 유지되는 것을 보여준다.

int* ptr = new int(3);
unsigned int myAddr = reinterpret_cast<unsigned int>(ptr);
int* restoredAddr = reinterpret_cast<int*>(myAddr);
assert(ptr == restoredAddr); // ptr과 restoredAddr은 동일하다.


 
요약: 우리가 보는 관점에선 값의 변형이 클 수 있지만  컴퓨터가 보는 관점에선 작다.
  ex) int(-10): 1111 1111 1111 1111 1111 1111 1111 0110 -> unsigned int(4294294967286
  즉 signed integer로 -10의 바이너리는 위와 같으며 최상위비트는 부호를 의미한다.
  하지만 unsigned int는 항상 양수이기 때문에  최상위비트를 부호 비트로 사용하지 않으며 나머지 1로 set된 비트들도 모두 set된 수로 본다. -10이 왜 저런 바이너리로 표시되는지 모른다면 2의 보수에 대해 찾아보자.



아까 static_cast에 대해 설명할 때 상속 관계더라도 위험한 경우를 발생시킬 수 있다고 했다.
아래 코드는 상위 클래스인 Foo와 하위 클래스인 A, B간에 캐스팅 연산을 보여준다.

class Foo
{
...
};

class A : public Foo
{
...
};

class B : public Foo
{
...
void FuncOfB();
};

Foo* f = new A();
A* a = static_cast<A*>(f); // 가능. f와 a는 상속 관계이기에
B* b = static_cast<B*>(f); // 가능. f와 b는 상속 관계이기에

b->FuncOfB(); // 위험  


위 코드에서 두 번째 캐스팅인 f를 B* 타입으로 변환하는 것은 위험할 수 있다. 포인터 f는 실제로 A 클래스의 메모리 영역을 가리키고 있으며, B*타입으로 캐스팅할 경우 포인터 b를 통해 B의 멤버 함수를 호출할 수 있게된다.
만약 B 클래스의 멤버 함수에  A에 없는 함수를 호출할 경우 크래시가 날 수 있다. 따라서 위험한 캐스팅이다. static_cast는 각 변수를 정적으로 바인딩시키에 f가 실제 가리키는 데이터(런타임)까지는 판단할 수 없고, 컴파일 시간에 오직 f의 타입과 대상(b)의 타입과의 상속 관계만 보고 판단한다.



◆ Best practice

1. 기본적으로 static_cast 연산자를 쓰자.  (안전)
- 알맞지 않은 형변환의 경우 컴파일러가 에러를 발생시키므로 위험 요소가 적다.

2. static_cast로 불가능 형변환일 경우 reinterpret_cast 연산자를 쓰자. (덜 안전)






댓글