개요
C++은 형변환(type casting)을 위한 네 가지 전용 키워드를 제공한다. 이들은 각각 고유한 목적과 사용 범위를 가지며, 기존 C 스타일의 괄호 캐스팅보다 명확하고 안전한 문맥을 제공한다. 해당 연산자는 다음과 같다:static_castdynamic_castconst_castreinterpret_cast
reinterpret_cast: 비논리적 재해석
reinterpret_cast는 메모리 표현을 전혀 다른 타입으로 해석하도록 강제한다. 이는 실제로 데이터를 변환하지 않으며, 단지 포인터나 값을 다른 타입처럼 취급하게 만든다. 주로 저수준 시스템 프로그래밍이나 바이트 조작에서 사용된다.
사용 제약: 피연산자는 포인터, 참조, 산술형, 함수 포인터 또는 멤버 포인터여야 한다.
예시:#include <iostream>
int main() {
int value = 65;
int* ptr = &value;
char* asChar = reinterpret_cast<char*>(ptr);
std::cout << *asChar; // 출력: 'A' (ASCII 기반)
}
이 코드는 정수 65를 가리키는 포인터를 문자형 포인터로 재해석하여 ASCII 문자 'A'를 출력한다. 하지만 엔디안에 따라 예측할 수 없는 결과를 낼 수 있으므로 주의가 필요하다.
const_cast: 상수성 제거
const_cast는 객체의 const 또는 volatile 속성을 제거하는 유일한 방법이다. 그러나 중요한 점은, 원본 객체가 처음부터 상수가 아니어야 한다는 것이다. 만약 상수 객체의 const를 제거하고 수정하려 하면 미정의 동작(undefined behavior)이 발생한다.
주요 용도: const가 아닌 외부 API와의 호환성 확보 — 즉, const 포인터를 non-const 함수에 전달할 때 사용.
예시:void modify(int* x) { *x = 42; }
const int fixed = 100;
const int* pFixed = &fixed;
// modify(pFixed); // 컴파일 오류
modify(const_cast<int*>(pFixed)); // 위험! 원본이 const이므로 UB
이 경우 fixed는 const로 선언되었기 때문에 수정 시도는 미정의 동작이다. 따라서 const_cast는 매우 신중히 사용해야 한다.
static_cast: 정적 형 변환
static_cast는 컴파일 타임에 결정되는 형변환이며, 런타임 체크 없이 변환을 수행한다. 대부분의 합법적인 변환에 사용 가능하며, 개발자가 타입 안정성을 보장해야 한다.
주요 사용 사례:
- 기본 자료형 간 변환 (예:
int→double) - 클래스 계층 내 상향/하향 캐스팅 (상향은 안전, 하향은 위험)
- void* 포인터로부터의 복원
- 명시적 변환 연산자 호출
double divide(int a, int b) {
return static_cast<double>(a) / static_cast<double>(b);
}
또한 상속 구조에서 부모 클래스 포인터를 자식 클래스로 다운캐스팅할 수 있지만, 실제 객체 타입이 맞지 않으면 오류가 발생한다.
dynamic_cast: 런타임 타입 검사
dynamic_cast는 상속 계층 내에서만 사용되며, 런타임에 실제 객체 타입을 확인한 후 안전하게 변환한다. 실패 시 포인터라면 nullptr, 참조라면 예외를 던진다.
전제 조건: 대상 클래스는 반드시 하나 이상의 가상 함수(virtual function)를 가져야 한다 (즉, 다형성이 있어야 함).
예시:class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {};
Base* create(bool wantDerived) {
return wantDerived ? new Derived() : new Base();
}
int main() {
Base* ptr = create(false);
Derived* d = dynamic_cast<Derived*>(ptr);
if (d) {
std::cout << "변환 성공";
} else {
std::cout << "변환 실패 (nullptr)";
}
delete ptr;
}
이 경우 ptr이 실제로 Derived 객체를 가리키지 않으므로 d는 nullptr이 된다. 반면 create(true)인 경우 성공한다.
비교 및 요약
| 연산자 | 런타임 체크 | 다형성 필요 | const 제거 가능 | 주요 용도 |
|---|---|---|---|---|
static_cast |
아니요 | 아니요 | 아니요 | 일반 변환, 다운캐스팅 (신뢰 기반) |
dynamic_cast |
예 | 예 | 아니요 | 안전한 다운캐스팅 |
const_cast |
아니요 | 아니요 | 예 | const/volatile 제거 |
reinterpret_cast |
아니요 | 아니요 | 아니요 | 포인터 재해석, 저수준 조작 |
실용 예제
다음 코드는 각 캐스트의 동작을 비교한다.#include <iostream>
class Parent {
public:
virtual ~Parent() {}
int x = 1;
};
class Child : public Parent {
public:
int y = 2;
};
int main() {
int raw = 97;
// reinterpret_cast: 포인터 재해석
char* cPtr = reinterpret_cast<char*>(&raw);
std::cout << "reinterpret: " << *cPtr << "\n"; // 'a'
// const_cast: const 제거
const int* cVal = &raw;
int* mutableVal = const_cast<int*>(cVal);
*mutableVal = 100;
std::cout << "const_cast 값: " << *mutableVal << "\n";
// static_cast: 하향 캐스팅 (위험)
Parent* basePtr = new Parent();
Child* unsafeChild = static_cast<Child*>(basePtr);
std::cout << "static_cast y: " << unsafeChild->y << "\n"; // 쓰레기 값
// dynamic_cast: 안전한 하향 캐스팅
Child* safeChild = dynamic_cast<Child*>(basePtr);
if (safeChild) {
std::cout << "동적 변환 성공\n";
} else {
std::cout << "동적 변환 실패 (null)\n";
}
delete basePtr;
return 0;
}