이 Step에서 다루는 것

  • “관계 없는 타입 포인터 캐스팅”이 왜 위험한지
  • C-Style 캐스팅이 왜 버그를 숨기기 쉬운지
  • 안전한 대안: static_cast/dynamic_cast 등 목적별 캐스팅

학습 목표

  • Dog* d = (Dog*)k; 같은 코드가 왜 UB인지 설명할 수 있다.
  • “어떤 메모리가 깨질지 예측 불가”라는 의미를 이해하고 설명할 수 있다.
  • 다운캐스팅이 필요할 때 dynamic_cast를 적용할 수 있다.

문제 예시: 관계 없는 타입으로 강제 캐스팅

class Knight {
public:
    virtual ~Knight() = default;
    int hp = 100;
    int attack = 20;
};

class Dog {
public:
    int age = 0;
};

Knight* k = new Knight();
Dog* d = (Dog*)k; // ❌ C-Style 강제 캐스팅
d->age = 10;      // ❌ Knight 객체 메모리를 Dog로 해석해서 기록 (UB)

핵심:

  • KnightDog는 상속 관계가 없는데도 억지 변환을 해버린 상태입니다.
  • 컴파일이 되더라도 “정상 동작”을 의미하지 않습니다.

왜 위험한가: “타입 해석 규칙”이 깨진다

  • 객체 메모리는 “그 타입 규칙”에 맞춰 읽고 써야 합니다.
  • 관계 없는 타입으로 캐스팅하면, 같은 주소를 다른 구조로 해석하게 됩니다.
  • 결과적으로 어떤 필드/vptr/패딩이 손상될지 구현·최적화·빌드 옵션에 따라 달라질 수 있어 예측 불가입니다.

메모리 오염 감각(개념도)

실제: Knight* k ──► [Knight 메모리 레이아웃]
강제: Dog* d    ──► [동일 주소를 Dog 레이아웃으로 해석]

d->age = 10; 수행 시:
  Knight 객체의 "어딘가"를 Dog::age 자리로 가정하고 기록
  -> 잘못된 위치 쓰기 -> 데이터 오염/크래시/비정상 분기 (UB)

중요한 점: “정확히 어떤 멤버가 깨진다”는 보장할 수 없습니다. 바로 이 불확실성이 UB의 본질입니다.


안전한 실전 규칙

  • C-Style 캐스팅 (T*)expr는 너무 많은 변환을 한 번에 허용해서 위험합니다.
  • 용도별로 C++ 캐스팅을 분리해서 씁니다.

기본 가이드:

  • 업캐스팅(자식 -> 부모): 보통 암시적 변환 또는 static_cast
  • 다운캐스팅(부모 -> 자식): dynamic_cast + nullptr 체크
  • 비트 재해석이 필요한 저수준 작업: reinterpret_cast (아주 제한적으로)
  • const 제거: const_cast (정말 필요한 경우만)

다운캐스팅 안전 예시:

Item* item = GetItem();

if (Weapon* w = dynamic_cast<Weapon*>(item)) {
    // Weapon으로 확인된 경우에만 접근
    // w->GetDamage();
}

체크 질문 (스스로 답해보기)

  • Dog* d = (Dog*)k;가 컴파일되더라도 왜 안전하지 않을까?
  • UB에서 “어떤 멤버가 깨질지 모른다”는 말이 왜 중요한가?
  • 다운캐스팅에서 dynamic_cast와 널 체크를 같이 쓰는 이유는?

profile
李家네_공부방

0개의 댓글