C | C++/Basic

[C++] 캐스팅(Casting)

HYEOKJUN 2025. 1. 30. 21:00
반응형

일반적인(C 스타일) 캐스팅의 문제

 C언어에서 사용되는 일반적인 캐스팅은 다음과 같이 사용합니다.

(변환형)변수

 위와 같이 매우 간단하게 특정 변수의 명시적으로 자료형을 변환할 수 있습니다.

 하지만, 일반적인 캐스팅에는 사소한 문제점이 존재합니다.

  1. 컴파일 타임에 타입 변환 유효성 체크하지 않음 -> 근원을 알기 힘든 런타임 오류를 발생시킬 가능성이 있음
  2. 특히 포인터 관련 자료형에 대한 캐스팅 시에 추적이 어려움 -> 의도를 알기 어려움 -> 디버깅 어려움
  3. 거의 모든 자료형으로 변환 가능 -> 안전성 부족, 오류 발생 가능성 증가

 위 문제들을 해결하기 위해 C++에서는 더 안전하고 명시적인 캐스팅을 위해서 또다른 Cast 연산자를 제공합니다.


C++ 캐스팅 연산자

 C++에서는 다음과 같은 4가지 Cast 연산자를 제공합니다.

static_cast<>()
dynamic_cast<>()
const_cast<>()
reinterpret_cast<>()

 위와 같은 캐스팅 방법은 더 안전하고 명시적인 자료형 변환을 지원합니다. 각 Cast 연산자마다 특정 상황에서 사용되기 때문에 사용 시에 유의해야 합니다.

 

static_cast

 static_cast컴파일 타임에 캐스팅을 수행하는 연산자입니다.

 static_cast는 다음과 같은 특징을 가집니다.

  • 컴파일 타임에 검사 및 변환
    • 타입 검사와 변환 가능 여부를 컴파일 타임에 확인하고, 캐스팅이 불가능하면 컴파일 에러를 발생시킵니다.
  • 논리적으로 변환이 가능한 캐스팅만 허용
    • 기본 자료형이나 클래스의 계층 구조간의 캐스팅과 같이 논리적으로 가능한 캐스팅만 허용하고, 캐스팅이 불가능하면 컴파일 에러를 발생시킵니다.
  • 다운 캐스팅 시 데이터 손실 가능성
    • 일반 C 스타일 캐스팅과 같이 크기가 큰 자료형에서 작은 자료형으로 캐스팅 시에 데이터 손실의 가능성이 있습니다. 

int a = 123;
double b = static_cast<double>(a);

 위 코드는 Int 변수 a를 Double 타입으로 캐스팅하여 변수 b에 할당하는 과정을 나타냅니다. Int와 Double 타입 모두 기본 자료형이므로 캐스팅이 가능합니다.


class Parent {};
class Child : public Parent {};

Parent* a = new Parent();
Child* b = static_cast<Child*>(a);

 위 코드는 Parent 포인터 변수 a를 Child 포인터로 캐스팅하여 변수 b에 할당하는 과정을 나타냅니다. Child는 Parent의 하위 계층 클래스이므로 포인터 변수간의 캐스팅이 가능합니다. 


class Parent1 {};
class Child1 : public Parent1 {};

class Parent2 {};
class Child2 : public Parent2 {};

Parent1* a = new Parent1();
Child2* b = static_cast<Child2*>(a); // 오류 발생!: 잘못된 형식 변환입니다.

 위 코드는 Parent1 포인터 변수 a를 Child2 포인터로 캐스팅하여 변수 b에 할당하는 과정을 나타냅니다.Child2는 Parent1의 하위 계층 클래스가 아니기 때문에 C 스타일 캐스팅과 다르게 static_cast를 사용하면 컴파일 에러를 발생시켜 사용자가 인지할 수 있게 됩니다.


int n = 123;

void* a = &n;
int* b = static_cast<int*>(a);

 위 코드는 변수 n을 가르키는 Void 포인터 변수 a를 실제로 사용할 수 있는 Int 포인터 변수로 변경하는 과정을 나타냅니다. 위와 같이 특히 Void 포인터를 변환할 때 일반적으로 static_cast를 사용하는 것이 좋습니다. 

 

dynamic_cast

 dynamic_cast런타임에 캐스팅을 수행하는 연산자로, 주로 계층 구조 간의 캐스팅에서 사용합니다.

 dynamic_cast는 다음과 같은 특징을 가집니다.

  • 런타임에 검사 및 변환
    • 타입 검사와 변환 가능 여부를 런타임에 확인합니다.
  • 안전한 다운 캐스팅
    • 클래스 계층 구조 간, 특히 상위 클래스 포인터 및 참조를 하위 클래스 포인터 및 참조로 변환합니다.
    • 포인터 변수 간 캐스팅이 불가능하면 nullptr을 반환합니다.
    • 참조 변수 간 캐스팅이 불가능하면 std::bad_cast 예외를 발생시킵니다.
  • 해당 클래스 내 가상 함수 필요
    • dynamic_cast를 사용하려면 해당 클래스 내에 하나 이상의 virtual 함수가 정의되어 있어야 합니다. 실행 시간에 타입 정보를 확인하기 위해서 virtual 함수 메모리 영역을 참조하기 때문입니다. 
  • 오버헤드 가능성
    • 런타임에 검사 및 변환을 수행하기 때문에, 성능에 영향을 줄 수 있습니다. 특히, 규모가 큰 프로그램에서는 주의해서 사용할 필요가 있습니다.

class Parent { virtual ~Parent() {} };
class Child : public Parent {};

Parent* a = new Parent();
Child* b = dynamic_cast<Child*>(a);

if (b)
{
	// 캐스팅 성공
}
else
{
	// 캐스팅 실패
}

 위 코드는 Parent 클래스 타입의 포인터 변수 a를 Child 클래스 타입의 포인터로 캐스팅하여 변수 b에 할당하는 과정을 나타냅니다. Child 클래스 타입은 Parent의 하위 계층 클래스이므로 포인터 변수간의 캐스팅이 가능합니다. 만약 캐스팅이 불가능하다면 nullptr를 반환하여 If문의 Else 영역의 코드를 실행할 것입니다.


class Parent { virtual ~Parent() {} };
class Child : public Parent { };

try 
{
	Parent* pa = new Parent();
	Parent& a = *pa;
	Child& b = dynamic_cast<Child&>(a);
	// ...
}
catch (const std::bad_cast& e)
{
	// 캐스팅 실패
}

  위 코드는 Parent 클래스 타입의 포인터 변수 pa를 참조 변수 a에 할당하고 참조 변수 a를 Child 클래스 타입의 참조로 캐스팅하여 변수 b에 할당하는 과정을 나타냅니다. Child 클래스 타입은 Parent의 하위 계층 클래스이므로 참조 변수간의 캐스팅이 가능합니다. 만약 캐스팅이 불가능하면 std::bad_cast 예외를 발생시켜 catch 영역의 코드를 실행할 것입니다.

 

const_cast

 const_cast포인터 변수에 상수 속성을 추가 및 제거하는 데 사용되는 연산자입니다. 이 연산자는 자주 사용되지는 않습니다.

 const_cast는 다음과 같은 특징을 가집니다.

  • 상수 속성 추가 및 제거
    • const로 선언된 포인터 변수의 상수 속성을 제거할 수 있습니다.
    • 거의 사용되지 않는 경우이지만, 비상수 포인터 변수를 상수 포인터로 변환할 수 있습니다.
  • 정의되지 않은 동작 발생 가능성
    • const로 선언된 변수를 수정하기 위해 const_cast를 사용하는 것은 정의되지 않은 동작(Undefined Behaviour)을 일으킬 수 있습니다.
    • 큰 위험성이 존재하기 때문에 가급적 사용하지 않는 것이 좋습니다.

void setValue(const int* source, int value)
{
	int* mSource = const_cast<int*>(source);
	*mSource = value;
}

int a = 123;
setValue(&a, 321);
cout << a << endl; // 출력: 321

위 코드는 source를 상수 포인터로 받아 source의 실제 데이터를 value로 변경하는 함수를 이용하여 a의 값을 321로 변경하는 과정을 나타냅니다. 매개 변수 source는 상수 포인터로, 값을 변경할 수 없지만 const_cast를 이용하여 비상수 포인터로 캐스팅하고 mSource에 할당하여 값을 수정할 수 있는 상태가 되었습니다.


int n = 123;

int* a = &n;
const int* A = const_cast<const int*>(a);

 위 코드는 비상수 포인터 변수 a를 상수 포인터로 캐스팅하여 A에 할당하는 과정을 나타냅니다. 비상수인 변수 a는 수정할 수 있는 상태이지만, 변수 A는 상수 포인터로서 수정할 수 없습니다.


const int A = 123;
int* a = const_cast<int*>(&A);
*a = 321; // 정의되지 않은 동작 발생 가능성

 위 코드는 상수 변수 A를 참조하도록 캐스팅하여 비상수 포인터 변수 a에 할당하여 변수 a를 통해 상수 변수 A를 수정하려는 과정을 나타냅니다. 실제로 상수 변수 A의 값은 실제로 변경되지 않으며 어떤 오류도 발생하지 않지만, 정의되지 않은 동작을 일으킬 가능성이 생깁니다. 따라서 위와 같은 작업은 권장되지 않습니다.

 

reinterpret_cast

 reinterpret_cast임의의 포인터 타입 간의 변환을 수행하는 연산자입니다. 이 연산자는 가장 강력하고 위험하지만 제대로 사용하면 유용하게 사용할 수 있습니다.

 reinterpret_cast는 다음과 같은 특징을 가집니다.

  • 정수(주소)와 포인터 간의 변환
    • 정수 값을 메모리 주소로 사용하여 포인터로 변환할 수 있습니다. 주로 하드웨어 영역에서 사용됩니다. 잘못된 사용은  정의되지 않은 동작(Undefined Behaviour)을 일으킬 수 있습니다.
    • 특정 포인터의 메모리 주소로 변환할 수 있습니다.
  • 포인터 타입 간의 변환
    • 포인터 변수의 타입을 다른 포인터 타입으로 변경합니다. 이때 실제 데이터의 비트 패턴을 변경하지 않고 타입만 변경합니다. 
    • 포인터이기 때문에 함수 포인터도 포함됩니다.

class A { public: int a; };
class B { public: int b; };

A* pA = new A();
pA->a = 123;

B* pB = reinterpret_cast<B*>(pA);
cout << pB->b << endl; // 출력: 123

 위 코드는 클래스 A를 클래스 B로 강제 변환하여 같은 상대 위치에 있는 변수 a, b의 관계를 파악하는 과정을 나타냅니다. 클래스 A와 클래스 B는 서로 어떤 관계도 없지만 변환 전 멤버 변수 a에 123을 할당한 상태에서 클래스 B로 변환했을 때 멤버 변수 b에 같은 값을 가지고 있는 것을 확인할 수 있습니다. 이는 타입 변환 시에 실제 데이터의 비트 패턴을 변경하지 않고 타입만을 변경한다는 것을 알 수 있습니다.

반응형