[C/C++] 캐스팅(Casting)

할랑말랑·2026년 3월 17일

C/C++

목록 보기
27/45

캐스팅(Casting)

하나의 타입을 다른 타입으로 변환하는 것
데이터의 타입을 명시적, 암묵적으로 바꾸는 과정을 의미

캐스팅이 필요한 이유

  • 서로 다른 타입 간의 연산을 위해
  • 상속 관계에서 부모-자식 클래스 간 변환
  • 함수 매개변수 타입 맞추기
  • 포인터 타입 변환
  • 정밀도 조정 (int ↔ double)

캐스팅의 위험성

  • 잘못된 캐스팅은 런타임 에러 발생
  • 데이터 손실 가능
  • 타입 안정성 저하
  • 예측 불가능한 동작

1. 암시적 캐스팅 (Implicit Casting)

  • 컴파일러가 자동으로 수행하는 타입 변환
  • 프로그래머가 명시하지 않아도 발생
  • 안전한 변환일 때만 자동 수행
  • 데이터 손실이 없거나 적을 때
#include <iostream>

// 기본 클래스
class Animal
{
public:
    // 가상 함수: 파생 클래스에서 이 함수를 재정의(override)할 수 있음을 나타냅니다.
    virtual void speak()
    {
        std::cout << "동물 소리" << std::endl;
    }
    // 상속을 사용하는 기본 클래스는 가상 소멸자를 갖는 것이 안전합니다.
    virtual ~Animal() {}
};

// Animal을 상속받는 파생 클래스
class Dog : public Animal
{
public:
    // override 키워드: 기본 클래스의 가상 함수를 재정의한다는 것을 컴파일러에 명확히 알립니다.
    void speak() override
    {
        std::cout << "멍멍!" << std::endl;
    }
};

int main()
{
    std::cout << "====== 1. 숫자 확장 (Numeric Promotion) ======" << std::endl;
    std::cout << "작은 타입 -> 큰 타입으로 변환 (데이터 손실 없음, 안전)" << std::endl;
    int i1 = 100;
    long l1 = i1; // int -> long
    std::cout << "int " << i1 << " -> long " << l1 << std::endl;

    float f1 = 3.14f;
    double d1 = f1; // float -> double
    std::cout << "float " << f1 << " -> double " << d1 << std::endl;

    char c1 = 'S'; // 'S'의 ASCII 코드는 83
    int i2 = c1;   // char -> int
    std::cout << "char '" << c1 << "' -> int " << i2 << std::endl;

    std::cout << "\n====== 2. 숫자 축소 (Narrowing Conversion) ======" << std::endl;
    std::cout << "큰 타입 -> 작은 타입으로 변환 (데이터 손실 위험!)" << std::endl;
    double d2 = 3.141592324;
    int i3 = d2; // double -> int (소수점 이하 데이터가 잘려나감)
    std::cout << "double " << d2 << " -> int " << i3 << std::endl;

    long l2 = 300000;
    short s1 = l2; // long -> short (short의 표현 범위를 초과하여 데이터가 손상됨)
    std::cout << "long " << l2 << " -> short " << s1 << " (값이 손상됨!)" << std::endl;

    std::cout << "\n====== 3. 포인터 형변환 ======" << std::endl;
    int i4 = 10;
    int* p1 = &i4;
    void* vp1 = p1; // 모든 데이터 포인터는 void*로 암시적 변환 가능 (안전)
    std::cout << "int 포인터 " << p1 << " -> void 포인터 " << vp1 << std::endl;

    std::cout << "\n====== 4. 클래스 상속 관계에서의 형변환 (Upcasting) ======" << std::endl;
    std::cout << "파생 클래스 -> 기본 클래스로 변환 (안전)" << std::endl;
    Dog dog; // 파생 클래스(자식)의 객체 생성

    // Dog* 타입의 포인터(&dog)가 Animal* 타입의 포인터(ani)로 암시적 변환됨
    Animal* ani = &dog;

    std::cout << "기본 클래스 포인터(Animal*)로 파생 클래스(Dog)의 함수 호출: ";
    // ani는 Animal 포인터지만, 실제 가리키는 객체는 Dog이므로 Dog의 speak()가 호출됨
    ani->speak(); // 이것이 바로 '다형성'입니다.

    return 0;
}

특징

  • 안전하고 자연스러움
  • 컴파일러가 자동 처리
  • 프로그래머 의도가 명확하지 않을 수 있음

2. 명시적 캐스팅 (Explicit Casting)

  • 프로그래머가 직접 지정하는 타입 변환
  • 위험할 수 있는 변환도 강제로 수행
  • 의도를 명확히 표현

C 스타일 캐스팅

  • (타입)값 형태
  • 간단하지만 위험
  • 어떤 종류의 캐스팅인지 불명확
  • 현대 C++에서는 권장하지 않음
#include <iostream>

// 기본 클래스
class Animal
{
public:
    virtual void speak()
    {
        std::cout << "동물 소리" << std::endl;
    }
    // 중요: 기본 클래스의 포인터로 파생 클래스 객체를 delete할 경우,
    // 소멸자가 virtual이 아니면 파생 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생할 수 있습니다.
    // 따라서 상속을 염두에 둔 클래스는 반드시 가상 소멸자를 선언해야 합니다.
    virtual ~Animal()
    {
        std::cout << "Animal 소멸자 호출" << std::endl;
    }
};

// 파생 클래스
class Dog : public Animal
{
public:
    void speak() override
    {
        std::cout << "멍멍!" << std::endl;
    }
    // Dog 클래스에만 있는 고유한 함수
    void wagTail()
    {
        std::cout << "강아지가 꼬리를 흔듭니다." << std::endl;
    }
    ~Dog()
    {
        std::cout << "Dog 소멸자 호출" << std::endl;
    }
};

int main()
{
    // --- 1. 기본 자료형 명시적 캐스팅 ---
    std::cout << "--- 기본 자료형 캐스팅 ---" << std::endl;
    double d1 = 3.14159;
    // (int)를 사용해 double 타입을 int 타입으로 강제 변환
    // 이 과정에서 소수점 이하 데이터가 잘려나가는 손실이 발생합니다.
    int i1 = (int)d1;
    std::cout << "double 값 " << d1 << "를 (int)로 캐스팅한 결과: " << i1 << std::endl;

    // --- 2. 포인터 변환 ---
    std::cout << "\n--- 포인터 캐스팅 ---" << std::endl;
    int i2 = 200;
    int* p1 = &i2;
    // (void*)를 사용해 int* 타입을 모든 타입을 가리킬 수 있는 void* 타입으로 변환
    void* vp1 = (void*)p1;
    std::cout << "int* 포인터 주소: " << p1 << std::endl;
    std::cout << "void* 포인터 주소: " << vp1 << std::endl;

    // (long)을 사용해 포인터(메모리 주소)를 정수 타입으로 강제 변환
    // 이는 매우 위험하며, 시스템 아키텍처(32비트/64비트)에 따라 결과가 달라질 수 있어 이식성이 떨어집니다.
    long addr1 = (long)vp1;
    std::cout << "포인터 주소를 (long)으로 캐스팅한 정수 값: " << addr1 << std::endl;

    // --- 3. 클래스 포인터 다운캐스팅 ---
    std::cout << "\n--- 클래스 포인터 다운캐스팅 ---" << std::endl;
    // Animal 포인터가 실제로는 Dog 객체를 가리키고 있음 (업캐스팅)
    Animal* ani = new Dog();
    std::cout << "ani 포인터는 Animal 타입이지만, 실제 객체는 Dog입니다." << std::endl;
    std::cout << "ani->speak() 호출: ";
    ani->speak(); // 다형성에 의해 Dog의 speak()가 호출됨

    // (Dog*)를 사용해 부모 클래스 포인터(Animal*)를 자식 클래스 포인터(Dog*)로 강제 변환
    // 이를 '다운캐스팅'이라고 합니다.
    Dog* dog = (Dog*)ani;

    // 이제 dog 포인터를 통해 Dog 클래스에만 존재하는 멤버 함수에 접근할 수 있습니다.
    std::cout << "다운캐스팅 후 dog->wagTail() 호출: ";
    dog->wagTail();

    // ani 포인터로 delete를 호출. Animal의 소멸자가 virtual이므로 Dog의 소멸자까지 안전하게 호출됩니다.
    delete ani;

    return 0;
}

C++ 스타일 캐스팅

C++은 4가지 명시적 캐스팅 연산자 제공

1. static_cast

  • 컴파일 타임에 타입 변환
  • 가장 일반적으로 사용
  • 논리적으로 타당한 변환에 사용
  • 안전성을 어느 정도 보장
#include <iostream>

// 상속 관계 예제를 위한 기본 클래스와 파생 클래스
class Animal
{
public:
    virtual void speak() { std::cout << "동물 소리" << std::endl; }
    // 상속 관계에서는 기본 클래스 소멸자를 virtual로 선언하는 것이 안전합니다.
    virtual ~Animal() {}
};

class Dog : public Animal
{
public:
    void speak() override { std::cout << "멍멍!" << std::endl; }
    // Dog 클래스에만 있는 고유 함수
    void wagTail() { std::cout << "강아지가 꼬리를 흔듭니다." << std::endl; }
};

// 열거형(enum) 예제
enum class Color { RED, GREEN, BLUE }; // enum class를 사용하는 것이 더 타입에 안전합니다.

int main()
{
    // --- 1. 기본 자료형 변환 (Numeric Cast) ---
    std::cout << "--- 1. 기본 자료형 변환 ---" << std::endl;
    double d1 = 3.14159;
    // 컴파일 시간에 타입을 확인하여 변환. 데이터 손실이 발생할 수 있음을 명시적으로 나타냄.
    int i1 = static_cast<int>(d1);
    std::cout << "double " << d1 << " -> static_cast<int> -> " << i1 << std::endl;

    // --- 2. 포인터 변환 ---
    std::cout << "\n--- 2. 포인터 변환 ---" << std::endl;
    int i2 = 200;
    int* p1 = &i2;
    // 모든 데이터 포인터 타입은 void*로 안전하게 변환 가능
    void* vp1 = static_cast<void*>(p1);
    std::cout << "int* " << p1 << " -> static_cast<void*> -> " << vp1 << std::endl;

    // void*를 원래 타입으로 되돌리는 것도 안전함
    int* restored_p1 = static_cast<int*>(vp1);
    std::cout << "void*를 다시 int*로 변환 후 값 접근: " << *restored_p1 << std::endl;

    // [주의] 관련 없는 타입의 포인터로 변환하는 것은 매우 위험!
    // double* p2 = static_cast<double*>(p1); // 컴파일 에러! static_cast는 이런 위험한 변환을 막아줌.
    // void*를 통해 변환은 가능하지만, 결과 포인터를 사용하는 것은 미정의 동작(Undefined Behavior)을 유발함.
    double* p2_dangerous = static_cast<double*>(vp1);
    std::cout << "void*를 관련 없는 double*로 변환은 가능하지만, 사용하면 안 됨!" << std::endl;

    // --- 3. 열거형(enum) 변환 ---
    std::cout << "\n--- 3. 열거형(enum) 변환 ---" << std::endl;
    Color c1 = Color::BLUE;
    // enum class는 int로 암시적 변환이 안 되므로, 반드시 명시적 캐스팅이 필요.
    int colorValue = static_cast<int>(c1);
    std::cout << "Color::BLUE -> static_cast<int> -> " << colorValue << std::endl;

    // --- 4. 클래스 계층 구조 변환 ---
    std::cout << "\n--- 4. 클래스 계층 변환 ---" << std::endl;
    // 가. 업캐스팅 (Upcasting): 파생 클래스 -> 기본 클래스 (안전)
    Dog dog_obj;
    // 업캐스팅은 보통 암시적으로 일어나지만, static_cast로 명시할 수도 있음.
    Animal* ani_ptr = static_cast<Animal*>(&dog_obj);
    std::cout << "업캐스팅 (Dog* -> Animal*): ";
    ani_ptr->speak();

    // 나. 다운캐스팅 (Downcasting): 기본 클래스 -> 파생 클래스 (개발자 책임)
    Animal* ani_ptr2 = new Dog(); // 기본 클래스 포인터가 실제로는 Dog 객체를 가리킴
    // 개발자가 ani_ptr2가 Dog 객체를 가리킨다는 것을 100% 확신하는 상황에서 사용.
    // dynamic_cast와 달리 실행 시간 검사를 하지 않아 더 빠르지만, 잘못 사용하면 큰 문제를 일으킴.
    Dog* dog_ptr = static_cast<Dog*>(ani_ptr2);
    std::cout << "다운캐스팅 (Animal* -> Dog*): ";
    dog_ptr->wagTail(); // 다운캐스팅을 통해 Dog의 고유 함수 호출 가능

    delete ani_ptr2;

    return 0;
}

기본 타입 변환
열거형 변환
포인터 변환 (상속 관계)
void 포인터 변환

특징

  • 컴파일 타임에 검사
  • 런타임 검사 없음
  • 빠르지만 안전성은 프로그래머 책임
  • 논리적으로 말이 되는 변환만

2. dynamic_cast

  • 런타임에 타입 변환 및 검사
  • 상속 관계에서만 사용
  • 안전한 다운캐스팅
  • 가상 함수가 있는 클래스에만 사용 가능
#include <iostream>

// --- 클래스 정의 ---
// dynamic_cast를 사용하려면, 클래스 계층에 반드시 하나 이상의 가상 함수가 존재해야 합니다.
// (보통 기본 클래스의 소멸자를 virtual로 만드는 것이 일반적입니다.)

class Animal
{
public:
    virtual void speak() { std::cout << "동물 소리" << std::endl; }
    // 기본 클래스의 소멸자는 반드시 virtual이어야 안전합니다.
    virtual ~Animal() { std::cout << "Animal 소멸자 호출" << std::endl; }
};

class Dog : public Animal
{
public:
    void speak() override { std::cout << "멍멍!" << std::endl; }
    // Dog 클래스에만 있는 고유 함수
    void wagTail() { std::cout << "강아지가 꼬리를 흔듭니다." << std::endl; }
    ~Dog() { std::cout << "Dog 소멸자 호출" << std::endl; }
};

class Cat : public Animal
{
public:
    void speak() override { std::cout << "야옹~" << std::endl; }
    ~Cat() { std::cout << "Cat 소멸자 호출" << std::endl; }
};

// Animal 포인터를 받아 Dog인지 확인하고 동작을 수행하는 함수
void tryToLetDogWagTail(Animal* p_animal)
{
    std::cout << "\n--- Dog인지 확인 시도 ---" << std::endl;
    p_animal->speak(); // 일단은 Animal로서 소리를 낼 수 있음

    // dynamic_cast 실행: p_animal이 실제로 Dog 객체를 가리키는지 런타임에 확인
    Dog* p_dog = dynamic_cast<Dog*>(p_animal);

    // 캐스팅 결과 확인 (가장 중요한 부분!)
    if (p_dog != nullptr)
    {
        // 캐스팅 성공! p_dog는 유효한 Dog 포인터.
        std::cout << "dynamic_cast 성공: 이 동물은 Dog가 맞습니다." << std::endl;
        p_dog->wagTail(); // 이제 Dog의 고유 함수를 안전하게 호출할 수 있음
    }
    else
    {
        // 캐스팅 실패! p_dog는 nullptr. p_animal은 Dog 객체를 가리키고 있지 않음.
        std::cout << "dynamic_cast 실패: 이 동물은 Dog가 아닙니다." << std::endl;
    }
}

int main()
{
    Animal* my_dog = new Dog();
    Animal* my_cat = new Cat();

    // 1. 성공 사례: Animal 포인터가 실제로 Dog 객체를 가리키고 있을 때
    tryToLetDogWagTail(my_dog);

    // 2. 실패 사례: Animal 포인터가 Dog가 아닌 Cat 객체를 가리키고 있을 때
    tryToLetDogWagTail(my_cat);

    // 할당된 메모리 해제
    delete my_dog;
    delete my_cat;

    return 0;
}

안전한 다운캐스팅
1. 부모 클래스 포인터 → 자식 클래스 포인터
2. 실제 객체 타입 확인
3. 실패 시 nullptr 반환 (포인터)
4. 실패 시 예외 발생 (참조) - bad_cast

특징

  • 가장 안전한 캐스팅
  • 런타임 검사로 실패 감지 가능
  • 다형성 있는 클래스만 가능
  • 성능 비용 있음

3. const_cast

  • const 속성만 추가하거나 제거
  • 타입 자체는 변경하지 않음
  • const 정확성을 우회
#include <iostream>

// const int*를 받아 내부에서 값을 바꾸려는 함수
// const 키워드는 이 함수가 포인터가 가리키는 값을 바꾸지 않겠다고 약속하는 것.
void modifyValue(const int* ptr)
{
    std::cout << "  (함수 진입) 포인터는 const 상태이므로 값 변경 불가." << std::endl;

    // const_cast를 사용해 포인터의 'const' 한정자를 일시적으로 제거.
    // 이는 컴파일러와의 약속을 깨는 행위이므로 매우 신중해야 함.
    int* modifiable_ptr = const_cast<int*>(ptr);

    // 이제 non-const 포인터를 통해 값을 수정.
    *modifiable_ptr = 999;
    std::cout << "  (함수 탈출) const_cast를 통해 값을 999로 변경함." << std::endl;
}

int main()
{
    // =================================================================
    // 사례 1: 원본 변수가 'const'가 아닌 경우 (동작은 하지만, 좋은 설계는 아님)
    // =================================================================
    std::cout << "--- 사례 1: 원본 변수가 non-const일 때 ---" << std::endl;
    int num = 100;
    // num 자체는 const가 아니지만, 포인터에 const를 붙여 "이 포인터를 통해서는 값을 바꾸지 않겠다"고 약속.
    const int* const_ptr_to_num = #

    std::cout << "변경 전 num 값: " << num << std::endl;
    modifyValue(const_ptr_to_num); // const 포인터를 함수에 전달
    std::cout << "변경 후 num 값: " << num << std::endl;

    // =================================================================
    // 사례 2: 원본 변수 자체가 'const'인 경우 (절대 금지! 미정의 동작 유발!)
    // =================================================================
    std::cout << "\n--- 사례 2: 원본 변수가 const일 때 (위험!) ---" << std::endl;
    const int const_num = 100; // 변수 자체가 상수로 선언됨

    std::cout << "변경 전 const_num 값: " << const_num << std::endl;
    // const_cast를 사용해 const 변수를 수정하려는 시도
    // 이 행위는 C++ 표준에서 '미정의 동작(Undefined Behavior)'으로 규정됨.
    // 프로그램이 즉시 비정상 종료될 수도 있고, 겉보기엔 동작하는 것처럼 보일 수도 있으며,
    // 전혀 예상치 못한 결과를 낼 수도 있음. 즉, 결과를 신뢰할 수 없음.
    int* dangerous_ptr = const_cast<int*>(&const_num);
    *dangerous_ptr = 999;
    std::cout << "변경 후 const_num 값: " << const_num << std::endl;
    std::cout << "  (값이 바뀐 것처럼 보일 수 있으나, 이는 미정의 동작의 한 예일 뿐입니다.)" << std::endl;

    return 0;
}

const 제거

  1. const 객체를 non-const로 변환
  2. 레거시 코드 호환성
  3. 주의해서 사용해야 함

const 추가

  1. non-const를 const로 변환
  2. 거의 사용하지 않음 (암시적 변환 가능)

특징

  • const 속성만 변경
  • 다른 타입 변환 불가
  • 위험하므로 최소한으로 사용

4. reinterpret_cast

  • 비트 패턴을 재해석
  • 가장 위험한 캐스팅
  • 거의 모든 포인터 변환 가능
  • 하드웨어 수준의 변환

포인터 타입 강제 변환

  1. 전혀 관련 없는 타입 간 변환
  2. int* → char*
  3. 객체 포인터 → 정수

하드웨어 접근

  1. 메모리 주소 직접 조작
  2. 임베디드 시스템
  3. 시스템 프로그래밍

특징

  • 아무런 검사 없음
  • 비트 패턴만 재해석
  • 매우 위험
  • 플랫폼 의존적
  • 이식성 낮음

1. 업캐스팅(Upcasting)

  • 자식 → 부모 클래스
  • 항상 안전
  • 암시적 변환 가능
  • 캐스팅 불필요
  • 정보 손실 없음 (is-a 관계)

2. 다운캐스팅 (Downcasting)

  • 부모 → 자식 클래스
  • 위험할 수 있음
  • 명시적 캐스팅 필요
  • 실제 객체가 자식 타입인지 확인 필요
  • dynamic_cast로 안전하게 수행

0개의 댓글