TIL_068 : C++ Casting

김펭귄·2025년 11월 23일

Today What I Learned (TIL)

목록 보기
68/104

오늘 학습 키워드

1. complete/sub object

class A {};
class B : A {};
class C : B {};

A* c = new C();

  • 이러한 구조에서 C객체 안에는 AB라는 subobject를 가짐

  • 반대로, Ccomplete object

  • complete object는 메모리상으로 클래스 전체(C)가 객체로 존재함을 의미
    다른 객체의 일부(subobject)가 아닌, 독립적으로 존재하는 객체

  • 이 객체의 상속받은 각 부분들은 이 객체 안에서의 subobject라 부름

2. 다형성 객체

  • 가상함수를 최소 하나 이상 가지는 객체를 다형성 객체라고 함

  • 가상함수를 이용하여 런타임 중에 다양한 파생클래스의 함수를 호출가능하므로

  • 가상함수가 없는 객체는 비다형성 객체

3. dynamic_cast

3.1. 문법

dynamic_cast < type-id > ( expression )

  • type_id는 클래스 포인터 또는 클래스 참조자이거나 void*여야만 함

  • 왜냐하면 void*는 범용적으로 사용하기 위해서.

class A {virtual void f() {}};	// 다형클래스
class B {virtual void f() {}};	// 다형클래스

A* pa = new A;
B* pb = new B;
void* pv = dynamic_cast<void*>(pa);	// pv는 A의 객체를 가리킴
pv = dynamic_cast<void*>(pb);		// 이제 pv는 B의 객체를 가리킴
  • 클래스 포인터/참조자는 애초에 dynamic_cast가 상속구조에서 서로 다른 계층으로의 변환을 위해 사용되기 때문

  • 반환값은 type_id이며, 포인터로 변환 실패시 nullptr을 반환, 참조자로 변환 실패시 bad_cast exception을 반환

3.2. 세부 설명

class Animal {virtual void speak() { /* */}};
class Dog : public Animal {void speak() override {/* */}};

Animal* a = new Dog;
Dog* dog = dynamic_cast<Dog*>(a);	// 다형성 객체여서 가능
  • 주로 부모클래스 포인터에서 자식클래스 포인터로 바꾸는 다운캐스팅에 사용됨

  • 다운캐스팅을 안전하게 하려면, 객체 포인터의 런타임 중 실제 객체 타입을 알아야 함

  • RTTI(run-time type information)를 통해, 현재 포인터가 complete object를 가리키는건지 subobject를 가리키는지 알 수 있음

  • 그래서 이 포인터를 안전하게 다른 클래스타입으로 변환할 수 있는지 확인할 수 있다(상속구조이면 안전)

  • 이렇게 런타임 중에, 계층구조를 확인하여 안전하게 변환이 이루어지는지 확인하고 변환해주므로 dynamic_cast는 다운캐스팅에 자주 사용된다

  • RTTI를 사용하므로, 다형성 객체에만 사용 가능.
    다형성객체에서 가상함수때문에 vtable이 생성되고, vtable과 관련하여 RTTI도 생성되기 때문.

  • 비다형성 객체는 vtable이 없으므로 RTTI도 없음. 따라서 비다형성에 사용하면 런타임 에러 발생

  • 근데 업캐스팅은 RTTI없어도 되므로 비다형성에 사용가능하긴하지만 보통 static_cast를 사용

3.3. 다운캐스팅 주의점

  • 사실 다운캐스팅 자체가 좋지 않음
  1. 유지보수와 안정성
    다운캐스트는 실제 객체 타입을 직접 알아야만 쓸 수 있다

  2. 객체지향 원칙에 맞지 않음
    OOP는 다형성을 이용하여, 부모클래스 포인터만으로 어떤 자식클래스든 동일한 인터페이스(가상 함수)를 통해 동작하도록 함

  3. 안전 문제
    base 포인터가 derived가 아니라, base인 complete object를 가리키고 있으면, 잘못된 다운캐스트로 런타임 에러 발생

  4. 성능 문제
    dynamic_cast는 런타임에 타입 체크를 하므로, 가상함수 호출보다 느림

  5. 대부분의 경우, 가상 함수 호출로 이미 원하는 동작을 구현 가능

  • 그럼에도 불구하고 다운캐스팅 해야한다면, dynamic_cast통해 안전하게 사용

  • 가상함수의 오버헤드 때문에 직접 다운캐스팅하여 파생클래스의 함수를 호춯하는 경우가 있다. 이때는 당연히 static_cast를 이용하여 성능을 끌어올림

4. static_cast

4.1. 문법

dynamic_cast < type-id > ( expression )

  • type_id로는 훨씬 더 폭넓게 사용 가능

    1. 클래스 포인터/참조: 업캐스팅/다운캐스팅 (상속 관계만 확인)
    2. 기본 타입: int→double, float→int, enum→int 등
    3. void*: 범용성
  • 캐스팅 잘 되면, 변환해서 반환해주고, 변환에 문제 있어 실패하면 아예 컴파일 에러 발생

4.2. 세부 설명

class Animal {};
class Dog : public Animal {};

Dog* d = new Dog;
Animal* a = static_cast<Animal*>(d);	// 업캐스팅
  • 다운캐스팅에는 RTTI가 필요하므로 런타임에 검사해주는 dynamic_cast로 안전하게 다운캐스팅함

  • 근데 업캐스팅은 RTTI 상관 없이, 컴파일 타임에 상속구조 보고 안전한지가 판단됨

  • 따라서 static_cast를 이용하여 컴파일 타임에 변환이 안전한지 판단하고, 문제없으면 컴파일 후 런타임에 변환해줌

  • 그래서 RTTI가 필요한 다운캐스팅에는 사용 안 하고, 캐스팅이 안전한 업캐스팅에 사용

  • 만약 실제 런타임 타입이 다를 경우에는 에러가 아니라 캐스팅해주어 반환해줌.
    개발자가 잘 감당해야함

class Animal { };
class Dog : public Animal { public: int k = 5; };

Animal* a = new Animal;    // Animal 객체만 할당 (k 없음)
Dog* d = static_cast<Dog*>(a);	// 잘못된 다운캐스팅. 반환은 해줌
d->k; // 원래 사용하지 않았던 메모리에 접근. 쓰레기값 나오거나, 에러 발생 가능
  • 뿐만 아니라, 단순 자료형 변환시에도 변환은 무조건 해주지만 값은 보장 못함.
    int->char 변환시 해주지만, 오버플로우 발생 가능

4.3. 비다형성에 RTTI가 없는 이유

  • 사실 비다형성 객체라 해도, 실제 타입은 런타임에 정해짐 A* a = new B();

  • 근데 왜 비다형성 객체는 RTTI를 가지지 않는지가 궁금하였음

  • 비다형성 객체는 가상함수가 없기에, 부모 포인터로 자식 객체를 가리켜도 멤버 함수 호출, 멤버 변수 접근 등은 항상 부모 타입 기준으로만 동작함

  • 다운캐스팅도 의미가 없는게, 어차피 가상함수도 없으니까 처음부터 자식클래스 포인터로 만들면 됨

  • 그래서 RTTI가 필요 없어 사용 안 함

5. const_cast

5.1. 문법

const_cast <type-id> (expression)

  • type_id : 이전 캐스팅들과 다르게 거의 대부분의 자료형으로 변환 가능

  • 변수, 포인터, 참조, 함수에 붙은 const(상수성) 또는 volatile(변동성) 속성을 일시적으로 없애거나 붙이고 싶을 때 사용

5.2. volatile

  • 언제든 값이 수없이 바뀔 수 있다고 지정하는 키워드

  • 값이 엄청 바뀌므로 컴파일러에게 최적화, 캐싱 등을 하지 말라고 함

  • 특히 프로그램 외부에서도 값을 바꿀 수도 있음

  • 그래서 최적화, 캐시하지 말고 메모리에서만 변수를 읽고 값 수정하도록 하여 안정성을 부여

5.3. 예시 코드

  • const제거하는 예시 코드
const int a = 10;
int* p = const_cast<int*>(&a);	// const 제거. 갑 수정 가능

int num = 42;
const int &ref = num;	// ref로 값 수정하면 에러
int &modRef = const_cast<int &>(ref);
  • const 추가하는 코드지만, 사실 이렇게 안 씀
int b = 30;
const int* pcb = const_cast<const int*>(&b); // const 추가
  • thisconst MyClass*를 의미.
    const_castMyClass*non const로 바꾸면서, 내부 값을 수정할 수 있게 됨
class MyClass {
public:
    void f() const {
        const_cast<MyClass*>(this)->x = 5;
    }
private:
    int x = 0;
};
  • volatile관련 코드
volatile int flag = 0;
int* pFlag = const_cast<int*>(&flag); // volatile 제거

int normal = 5;
volatile int* vp = const_cast<volatile int*>(&normal); // volatile 추가

6. reinterpret_cast

6.1. 문법

reinterpret_cast < type-id > ( expression )

  • 모든 포인터를 다른 포인터 타입으로 변환할 수 있다

  • 또한 모든 정수 타입을 모든 포인터 타입으로 변환할 수 있고, 그 반대도 가능

6.2. 세부 설명

  • 메모리에 있는 값(bit)은 그대로 둔채, 포인터자료형만을 바꿔주는 기능

  • 보통 low-level에서 사용

  • 애초에 안전하지 않은 캐스팅으로, 왠만해선 다른 캐스팅 방법을 쓰는게 좋음

  • One_class* -> Unrelated_class* 처럼 관계없는 포인터 타입을 변환해주기에 안전하지 않음

  • 그래서 애초에 반환값도 안전하지 않아 사용하기 힘듦

  • 특히 상속관계의 클래스에는 꼭 static_castdynamic_cast사용

6.3. 예시 코드

  • 타입 변환
int a = 1234;
char* p = reinterpret_cast<char*>(&a); // int* → char*

void* v = &a;
int* ip = reinterpret_cast<int*>(v);   // void* → int*
  • 해시
unsigned short Hash( void *p ) {
   unsigned int val = reinterpret_cast<unsigned int>( p );
   return ( unsigned short )( val ^ (val >> 16));
}
  • 데이터 p의 메모리 주소값을 바로 unsigned int로 변환함
    그리고 주소의 상위 16bit와 하위 16bit를 xor하여 hash로 사용

  • 주소가 다르면 해시값이 같을 확률이 낮아 포인터를 바로 정수형으로 변환시켜 해시로 사용

profile
반갑습니다

0개의 댓글