본문 바로가기

프로그래밍 언어들/C++

CHAPTER16 - C++의 형변환 연산자

1. C++에서의 형 변환 연산

C언어의 형 변환 연산자는 강력하기 때문에, 아래 예제에서 보이는 실수를 해도 컴파일러는 이를 잡아내지 못한다.

class car
{
private:
    int fuelgauge;
public:
    car(const int fuel) : fuelgauge(fuel) {}
    void showcarstate(void) const
    {
        cout << "잔여 연료량 : " << fuelgauge << endl;
    }
};

class truck : public car
{
private:
    int freightweight;
public:
    truck(const int fuel, const int w) : car(fuel), freightweight(w) {}
    void showtruckstate(void) const
    {
        showcarstate();
        cout << "화물의 무게 : " << freightweight << endl;
    }
};

int main(void)
{
    car *pcar1      = new truck(80, 200);
    truck *ptruck1  = (truck*)pcar1;
    ptruck1->showtruckstate();
   
    cout << endl;

    car *pcar2      = new car(120);
    truck *ptruck2  = (truck*)pcar2;
    ptruck2->showtruckstate();

    return 0;
}
잔여 연료량 : 80
화물의 무게 : 200

잔여 연료량 : 120
화물의 무게 : -33686019


두 번째 형 변환의 경우, 조금만 분석해보면 프로그래머의 실수임을 판단할 수 있지만,

첫 번째 형 변환의 경우, 실수인지 아닌지를 쉽게 판단할 수 없다. 결과만 놓고보면 컴파일도 실행결과에도 문제가 없지만,

불필요한 형 변환이기 때문이다.

이러한 유형의 논란과 문제점 때문에 C++에서는 다음과 같이 총 4개의 연산자를 추가로 제공한다.

- static_cast

- const_cast

- dynamic_cast

-reinterpret_cast

위의 형 변환 연산자들을 사용하면 프로그래머는 자신이 의도한 바를 명확히 표시할 수 있다.

따라서 컴파일러도 실수를 지적해줄 수 있고, 코드를 직접 작성하지 않은 프로그래머들도 실수 여부를 판단할 수 있다.


1.1 dynamic_cast : 상속관계에서의 안전한 형 변환

dynamic_cast 형 변환 연산자는 다음의 형태를 갖는다.

=> dynamic_cast<T>(expr)

즉, '<>' 사이에 변환하고자 하는 자료형의 이름을 두되, 객체의 포인터 또는 참조형이 와야하며,

'()' 사이에는 변환의 대상이 와야 한다. 그리고 요구한 형 변환이 적절한 경우에는 형 변환된 데이터를

반환하지만, 적절하지 않은 경우에는 컴파일 시 에러가 난다. 여기서 말하는 적절한 형 변환은 다음과 같다.

"상속관계에 놓여 있는 두 클래스 사이에서 유도 클래스의 포인터 및 참조형 데이터를, 기초 클래스의

포인터 및 참조형 데이터로 형 변환하는 경우."

int main(void)
{
    // compile error
    car *pcar1      = new truck(80, 200);
    truck *ptruck1  = dynamic_cast<truck*>(pcar1);

    // compile error
    car *pcar2      = new car(120);
    truck *ptruck2  = dynamic_cast<truck*>(pcar2);

    // compile success
    car *ptruck3    = new truck(70, 150);
    car *pcar3      = dynamic_cast<car*>(ptruck3);

    return 0;
}


사실 dynamic_cast 연산자를 사용했다는 것은 다음의 의미가 담겨있다.

"상속관계에 있는 유두 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환한다."


1.2 static_cast : A 타입에서 B 타입으로

static_cast 형 변환 연산자는 다음의 형태를 갖는다(dynamic_cast 연산자와 동일한 형태이다).

=> static_cast<T>(expr)
static_cast 연산자를 사용하면, 유도 -> 기초 클래스로 형 변환 뿐만 아니라, 기초 -> 유도 클래스로 형 변환이

가능하다. 하지만 그에 대한 책임은 프로그래머가 져야 한다.
따라서, static_cast는 dynamic_cast 연산자와 달리, 보다 많은 형 변환을 허용한다. 그러나 dynamic_cast를 사용할 수 있는

경우에는 해당 연산자를 사용하여 안정성을 높여야 한다.

또한, static_cast 형 변환 연산자는 기존 자료형 간의 형 변환도 허용한다.

그렇다면, C언어의 형 변환 연산자와의 차이점이 없을 수도 있으나, 아래 예제를 참고하자.

int main(void)
{
    const int num   = 20;
    int *ptr        = (int*)&num;
    *ptr            = 30;
    cout << *ptr << endl;

    float *adr  = (float*)ptr;
    cout << *adr << endl;

    return 0;
}
30
4.2039e-044


위의 예제에서 보인 형 변환은, static_cast 연산자로는 불가능한 형 변환이다. 따라서 여전히 static_cast는

C언어의 형 변환 연산자보다 적은 것을 허용하고 있다. 이로 인해 static_cast를 보는 순간 다음과 같이 판단할 수 있다.

"상속관계에 있는 클래스의 포인터 및 참조형 데이터의 형 변환인가? 아니면 기본 자료형 데이터의 형 변환인가?"


1.3 const_cast : const 성향을 삭제하라

C++에서는 포인터와 참조자의 const 성향을 제거하는 형 변환을 목적으로, 다음의 형 변환 연산자를 제공한다.

=> const_cast<T>(expr)

사실 이러한 연산은 const의 가치를 떨어뜨리는 것이라고 생각하는 분들도 계시지만, 그 이면을 살펴보면,

나름의 의미를 발견할 수 있다.

주로, const_cast 형 변환 연산은, 함수의 인자전달 시 const 선언으로 인한 형(type)의 불일치가 발생해서

인자의 전달이 불가능한 경우에 유용하게 사용이 된다.

1.4 reinterpret_cast : 상관없는 자료형으로의 형 변환

reinterpret_cast 연산자는 전혀 상관이 없는 자료형으로의 형 변환에 사용이 되며, 기본적인 형태는 다음과 같다.

=> reinterpret_cast<T><expr>

reinterpret_cast 연산자는 포인터를 대상으로 하는, 그리고 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다.

int main(void)
{
    int num = 72;
   
    int *ptr= &num;

    // 주소 값을 정수로 변환
    int adr = reinterpret_cast<int>(ptr);
    cout << "Addr : " << adr << endl;

    // 정수를 다시 주소 값으로 변환
    int *rptr   = reinterpret_cast<int*>(adr);
    cout << "value : " << *rptr << endl;

    return 0;
}
Addr : 11926080
value : 72


크게 사용 의미를 부여하긴 어렵겠지만, reinterpret_cast 연산자가 포인터와 관련이 있는 모든 유형의

형 변환을 허용한다는 사실으 뒷받침하기도 한다.


1.5 dynamic_cast 두 번째 이야기 : Polymorphic 클래스 기반의 형 변환

지금까지 설명한 내용을 잘 이해했다면, 상속과 관련된 형 변환에 대한 내용은 다음과 같다.

- 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는

dynamic_cast 연산자를 사용한다.
- 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는

static_cast 연산자를 사용한다.

그란 dynamic_cast 연산자도 기초 -> 유도 클래스의 포인터 및 참조형 데이터의 형 변환을 허용한다.

다음의 조건만 만족하면 말이다.

=> 기초 클래스가 'Polymorphic 클래스'이다.

즉, 하나 이상의 가상 함수를 지니는 클래스와 상속 관계에 놓여있으면, dynamic_cast 연산자를 이용하여

기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 변환이 가능하다.

class A
{
public:
    virtual void show(void)
    {
        cout << "A's show" << endl;
    }
};
class B : public A
{
public:
    void show(void)
    {
        cout << "B's show" << endl;
    }
};
int main(void)
{
    A *aptr = new B;

    B *bptr = dynamic_cast<B*>(aptr);

    bptr->show();

    return 0;
}
B's show


만약 A 클래스의 함수가 'virtual'로 선언되어 있지 않았다면 에러가 발생했을 것이다.

위의 연산이 성공한 이유는, 포인터 변수 'aptr'이 실제 가리키는 객체가 B 객체이기 때문이다.

즉, aptr이 가르키는 객체를 B 클래스형 포인터 변수가 함께 가리켜도 문제가 되지 않기 때문에 성공한 것이다.

그럼 다음 예제를 보도록 하자.

int main(void)
{
    A *aptr = new A;

    B *bptr = dynamic_cast<B*>(aptr);

    bptr->show();

    return 0;
}


위의 경우에는 aptr이 가르키는 대상을 bptr이 가르킬 수 없는 상황이다. 따라서 이러한 경우에는

형 변환의 결과로 NULL 포인터가 반환된다.

즉, dynamic_cast는 안정적인 형 변환을 보장한다. 특히 컴파일 시간이 아닌 실행시간에 안정성을 검사하도록

컴파일러가 바이너리 코드를 생성한다는 점에 주목할 필요가 있다.

그만큼 실행속도가 늦어지지만, 그만큼 안정적인 형 변환이 가능한 것이다.

반대로 static_cast의 경우에는, 컴파일러는 무조건 형 변환이 되도록 바이너리 코드를 생성하기 때문에,

실행속도는 빠르지만, 그로 인한 실행의 결과는 프로그래머가 책임져야 한다.


1.6 bad_cast 예외

dynamic_cast 연산자를 이용한 형 변환의 과정에서 발생할 수 있는 예외이다.

class A
{
public:
    virtual void show(void)
    {
        cout << "A's show" << endl;
    }
};
class B : public A
{
public:
    void show(void)
    {
        cout << "B's show" << endl;
    }
};
int main(void)
{
    A a1;
    A &aref = a1;

    try
    {
        B &bref = dynamic_cast<B&>(aref);
    }
    catch(bad_cast expt)
    {
        cout << expt.what() << endl;
    }

    return 0;
}
Bad dynamic_cast!


위의 예제에서는 aref가 실제 참조하는 객체가 A객체이기 때문에, B의 참조형으로 참조하는 것은

안전하지 못하다. 참조형을 대상으로 dynamic_cast 연산을 진행할 경우참조자를 대상으로는 NULL을

반환할 수 없기 때문에 이러한 상황에서는 'bad_cast' 예외가 발생한다.


출처 : 윤성우 열혈 C++ 프로그래밍