Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C++] C++에서의 형 변환 연산 본문

Programming/C++

[Programming/C++] C++에서의 형 변환 연산

scii 2021. 2. 8. 02:06

C++에서는 C스타일의 형 변환 연산자를 가리켜 '오래된 C스타일 형 변환 연산자(Old C-style cast operator)'라 부르기도 한다. 이렇듯 C스타일의 형 변환 연산자는 C언어와의 호환성을 위해서 존재할 뿐, C++에서는 새로운 형 변환 연산자와 규칙을 제공하고 있다.

#include <iostream>

#pragma warning(disable: 4996)

using std::cout;
using std::cin;
using std::endl;


class Car {
private:
    int fuelGauge;

public:
    Car(int fuel) : fuelGauge(fuel) {}

    void ShowCarState() const {
        cout << "잔여 연료량: " << fuelGauge << endl;
    }
};

class Truck : public Car {
private:
    int freightWeight;

public:
    Truck(int fuel, int weight) : Car(fuel), freightWeight(weight) {}

    void ShowTruckState() const {
        Car::ShowCarState();
        cout << "화물의 무게: " << freightWeight << endl;
    }
};


 int main(const int argc, const char* const argv[]) { 
     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

*/
Car* pcar1 = new Truck(80, 200);
Truck* ptruck1 = (Truck*)pcar1;

위의 문장은 pcar1 포인터가 가리키는 대상이 실제로 Truck객체이기 때문에 문제가 되지 않는다. 하지만 다음과 같은 문장은 문제를 야기한다.

Car* pcar2 = new Car(120);
Truck* ptruck2 = (Truck*)pcar2;

기초 클래스의 포인터 형을 유도 클래스의 포인터 형으로 형 변환하는 것은 일반적인 경우의 형 변환이 아니다. 따라서 이 상황에서 이것이 프로그래머의 의도인지, 아니면 실수인지 알 방법이 없다.

그런데 C스타일의 형 변환 연산자는 컴파일러로 하여금 이러한 일이 가능하게 한다. '무적의 형 변환 연산자'이기 때문이다.

C++에서는 다음과 같이 총 4개의 연산자를 추가로 제공하면서 용도에 맞는 형 변환 연산자의 사용을 유도한다.

  • static_cast
  • const_cast
  • dynamic_cast
  • reinterpret_cast

위의 형 변환 연산자들을 사용하면 프로그래머는 자신이 의도한 바를 명확히 표시할 수 있다. 따라서 컴파일러도 프로그래머의 실수를 지적해 줄 수 있고, 코드를 직접 작성하지 않은 프로그래머들도 코드를 직접 작성한 프로그래머의 실수여부를 판단할 수 있다.


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

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

dynamic_cast<T>(expr)

즉, < > 사이에 변환하고자 하는 자료형의 이름을 두되, 객체의 포인터 또는 참조형이 와야 하며, ( ) 사이에는 변환의 대상이 와야 한다. 그리고 요구한 형 변환이 적절한 경우에는 형 변환된 데이터를 반환하지만, 요구한 형 변환이 적절하지 않은 경우에는 컴파일 시 에러가 발생한다. 여기서 말하는 적절한 형 변환은 다음의 경우를 뜻한다.

상속관계에 놓여 있는 두 클래스 사이에서 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환하는 경우.

다음의 예제를 보자.

 int main(const int argc, const char* const argv[]) { 
     Car* pcar1 = new Truck(80, 200);
     // 컴파일 에러
     Truck* ptruck1 = dynamic_cast<Truck*>(pcar1);
     ptruck1->ShowTruckState();
     cout << endl;

     Car* pcar2 = new Car(120);
     // 컴파일 에러
     Truck* ptruck2 = dynamic_cast<Truck*>(pcar2);
     ptruck2->ShowTruckState();

     Truck* ptruck3 = new Truck(70, 150);
     // 컴파일 OK
     Car* pcar3 = dynamic_cast<Car*>(ptruck3);

     return 0;
}

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

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

그래서 이에 반하는 것은 컴파일 오류가 발생하는 대신에 이에 부합하는 것은 컴파일 오류가 발생하지 않는 것이다.

다음의 형 변환 명령은 dynamic_cast로 형 변환했을 시, 컴파일 오류가 발생하는 명령이다. 그러나 이러한 형 변환을 의도적으로 진행해야할 때도 있을 것이다.

Car* pcar1 = new Truck(80, 200);
Truck* ptruck1 = dynamic_cast<Truck*>(pcar1);

그렇다면 이러한 형 변환을 의도적으로 진행한다는 것을 명시하기 위한 형 변환 연산자를 사용해야 한다. 그리고 그에 맞는 형 변환 연산자가 바로 "static_cast" 연산자이다.


static_cast : A타입에서 B타입으로 형 변환

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

static_cast<T>(expr)
#include <iostream>

using std::cout;
using std::cin;
using std::endl;


class Car{
private:
    int fuelGauge;
    
public:
    Car(int fuel) : fuelGauge(fuel){}
    
    void ShowCarState(){
        cout<<"잔역 연료량: "<<fuelGauge<<endl;
    }
};

class Truck : public Car{
private:
    int freightWeight;
    
public:
    Truck(int fuel, int weight)
    : Car(fuel), freightWeight(weight){}

    void ShowTruckState(){
        Car::ShowCarState();
        cout<<"weight: "<<freightWeight<<endl;
    }
};


int main(int argc, const char * argv[]) {
    Car* pcar1 = new Truck(80, 200);
    Truck* ptruck1 = static_cast<Truck*>(pcar1);
    ptruck1->ShowTruckState();
    cout<<endl;

    Car* pcar2 = new Car(120);
    Truck* ptruck2 = static_cast<Truck*>(pcar2);
    ptruck2->ShowTruckState();

    return 0;
}

/* 결과

잔역 연료량: 80
weight: 200

잔역 연료량: 120
weight: 0

*/

static_cast 연산자는 dynamic_cast 연산자와 달리, 보다 많은 형 변환을 허용한다. 하지만 그에 따른 책임도 프로그래머가 져야 하기 때문에 신중하게 선택해야 한다. 
dynamic_cast 연산자를 사용할 수 있는 경우에는 dynamic_cast 연산자를 사용해서 안정성을 높여야 하며, 그 이외의 경우에는 정말 책임질 수 있는 상황에서만 제한적으로 static_cast 연산자를 사용해야 한다.

보다 빠른 static_cast 연산자

dynamic_cast 연산자보다 static_cast 연산자를 사용했을 때 연산의 속도가 더 빠르다. 
따라서 이러한 이유로 dynamic_cast 연산자를 사용해도 되는 상황에서 조차 static_cast 연산자를 사용하는 경우가 적지 않다.

static_cast 연산자는 기본 자료형 데이터간의 형 변환에도 사용이 된다.

int num1 = 20, num2 = 3;
double result = 20 / 3;

정수형 나눗셈의 결과로 6이 출력되기 때문에 실수형 나눗셈을 진행하려면 다음과 같이 나눗셈 문장을 구성해야한다.

double result = (double)20 / 3;

// 혹은

double result = double(20) / 3;

C++에서는 static_cast 연산자를 이용한 다음의 문장구성을 더 추천한다.

double result = static_cast<double>(20) / 3;

static_cast연산자와 C언어의 형 변환 연산자의 차이

static_cast연산자는 '기본 자료형 간의 형 변환'과 '클래스의 상속관계에서의 형 변환'만 허용한다. 그런데 C언어의 형 변환 연산자는 다음과 같이 형 변환도 허용하기 때문에 여전히 static_cast 연산자의 사용은 의미를 갖는다.

const int num = 20;
// const 상수의 포인터는 const 포인터이다.
int* ptr = (int*)&num;
// const 상수 num의 값이 실제로 변경된다.
*ptr = 30;
// 30이 출력된다.
cout << *ptr << endl;

// int형 포인터를 float형으로 변환
float* adr = (float*)ptr;
// 저장된 데이터를 float형으로 해석해서 출력
cout << *adr << endl;

위의 예제에서 보인 형 변환은 static_cast 연산자로는 불가능한 형 변환이다. 즉, 여전히 static_cast 연산자는 C언어의 형 변환 연산자보다 적은 것을 허용하고 있다. 이로 인해서 static_cast 연산자를 보는 순간 다음과 같이 판단할 수 있다.

상속관계에 있는 클래스의 포인터 및 참조형 데이터의 형 변환 혹은 기본 자료형 데이터의 형 변환

const_cast : const의 성향을 삭제

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

const_cast<T>(expr)

위의 연산자를 이용해서 const로 선언된 참조자, 그리고 포인터의 const 성향을 제거하는 것이 의미가 있을까? 다음의 예제를 살펴보자.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;


void ShowString(char* str){
    cout<<str<<endl;
}

void ShowAddResult(int& n1, int& n2){
    cout<<n1+n2<<endl;
}


int main(int argc, const char * argv[]) {
    const char* name = "test";
    ShowString(const_cast<char*>(name));
    
    const int& num1 = 100;
    const int& num2 = 200;
    ShowAddResult(const_cast<int&>(num1), const_cast<int&>(num2));
    
    return 0;
}

/* 결과

test
300

*/

위의 예제에서 포인터 변수 name은 const char* 형이고, 매개변수는 char* 형이다. 따라서 name은 정의된 함수의 인자로 전달될 수 없다. 참고로 name이 char* 형이고, 매개변수가 const char* 형이라면 인자로 전달 가능하다.
인자로 전달을 위해서 포인터 변수 name의 const를 제거하는 형태로 형 변환을 진행하고 있다.

이와 유사하게 인자의 전달을 위해 const int& 형 데이터를 int& 형으로 형 변환하고 있다.

이렇듯 const_cast 형 변환 연산은, 함수의 인자 전달 시 const 선언으로 인한 형(type)의 불일치가 발생해서 인자의 전달이 불가능한 경우에 유용하게 사용된다.

그러나 const_cast를 사용하면 const 선언의 의미가 반감된다. 때문에 이러한 이유로 const_cast 연산자의 긍정적인 측면이 잘 드러나는 경우에만 제한적으로 사용해야 한다.

const_cast 연산자는 volatile의 성향을 제거하는데도 사용할 수 있다. 

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

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

reinterpret_cast<T>(expr)

예를 들어 다음과 같이 클래스가 정의되어 있다고 가정해보자.

class SimpleCar { ... };

class BestFriend { ... };

위의 두 클래스는 상속으로 관계를 맺은 것도 아니니, 서로 전혀 상관없는 클래스이다. 그런데 이 두 클래스를 대상으로 다음과 같은 코드를 작성할 때 사용되는 것이 reinterpret_cast 연산자이다.

SimpleCar* car = new Car;

BestFriend* fren = reinterpret_cast<BestFriend*>(car);

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

그렇다면 fren 포인터 변수는 어떻게 동작할까? 그것은 알 수 없다. 왜냐하면 그 결과는 컴파일 환경에 따라 달라지기 때문이다. 이렇듯 위의 경우에는 형 변환이 가능하긴 하지만 의미를 부여할 순 없다.

reinterpret_cast 연산자는 다음 예제에서 보이는 것과 같이 의미 있게 사용할 수 있다. 

#include <iostream>

using std::cout;
using std::cin;
using std::endl;


int main(int argc, const char * argv[]) {
    int num = 0x00010203;
    char* ptr = reinterpret_cast<char*>(&num);
    
    for(int i=0; i<sizeof(num); i++){
        cout << static_cast<int>(*(ptr + i)) << endl;
    }

    return 0;
}

/* 결과

3
2
1
0

*/

int형 정수에 바이트 단위 접근을 위해 int 포인터를 char형 포인터로 형 변환하고 있다. 그리고 바이트 단위 데이터를 문자가 아닌 정수의 형태로 출력하기 위해서 char형 데이터를 int형으로 변환하고 있다.

reinterpret_cast 연산자는 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다. 따라서 다음과 같은 문장의 구성도 가능하다.

int num = 55;
int* ptr = &num;

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

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

위의 경우 크게 의미를 부여하긴 어렵겠지만, 특정 상황에서는 이러한 유형의 연산이 유용하게 사용되기도 하며, 이는 reinterpret_cast 연산자가 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다는 사실을 뒷받침하기도 한다.


dynamic_cast : Polymorphic 클래스 기반의 형 변환

  • 상속관계에 놓여있는 두 클래스 사이에서, 파생 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 dynamic_cast 연산을 사용한다.
  • 반대로, 상속관계에 놓여있는 두 클래스 사이에서, 기초 클래스의 포인터 및 참조형 데이터를 파생 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 static_cast 연산자를 사용한다.

하지만 다음의 조건을 만족한다면, dynamic_cast 연산자도 기초 클래스의 포인터 및 참조형 데이터를 파생 클래스의 포인터 및 참조형으로의 형 변환을 허용한다. 

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

Polymorphic 클래스란, 하나 이상의 가상함수를 지니는 클래스를 뜻한다.그러니 상속관계에 놓여 있는 두 클래스 사이에서, 기초 클래스에 가상함수가 하나 이상 존재하면, dynamic_cast 연산자를 이용해서 기초 클래스의 포인터 및 참조형 데이터를 파생 클래스의 포인터 및 참조형 데이터로 변환이 가능하다.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

// Polymorphic Class
class SoSimple {
public:
    virtual void ShowSimpleInfo() const {
        cout << "SoSimple Base Class" << endl;
    }
};

class SoComplex : public SoSimple {
public:
    virtual void ShowSimpleInfo() const override {
        cout << "SoComplex Derived Class" << endl;
    }
};

int main(int argc, const char * argv[]) {
    SoSimple* simPtr = new SoComplex;
    SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);
    comPtr->ShowSimpleInfo();

    return 0;
}

/* 결과

SoComplex Derived Class

*/

SoSimple 클래스가 Polymorphic 클래스이므로 SoComplex 클래스도 Polymorphic 클래스이다.

기초 클래스인 SoSimple형 포인터 변수 simPtr을 파생 클래스 SoComplex형 포인터로 형 변환하고 있다. 그런데 기초 클래스인 SoSimple이 Polymorphic 클래스이므로 dynamic_cast 연산자로 형 변환이 가능하다.

파생 클래스의 포인터 및 참조형으로의 형 변환을 시도할 때, 사용할 수 있는 두 연산자는 다음과 같다.

  1. dynamic_cast
  2. static_cast

그렇다면 이 둘에는 어떤한 차이가 있을까? 형 변환을 시도한다는 사실에는 차이가 없지만, 그 결과에는 큰 차이가 있다.

SoSimple* simPtr = new SoComplex;

SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);

위의 dynamic_cast 연산이 성공한 이유는 포인트 변수 simPtr이 실제 가리키는 객체가 SoComplex 객체이기 때문이다.
즉, 포인터 변수 simPtr이 가리키는 객체를 SoComplex형 포인터 변수 comPtr이 함께 가리켜도 문제되지 않기 때문에 성공한 것이다.

그렇다면 다음과 같이 변경하면 어떻게 될까?

SoSimple* simPtr = new SoSimple;

SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);

이 경우에는 simPtr이 가리키는 대상을 comPtr이 가리킬 수 없는 상황이다. 따라서 이러한 경우에는 형 변환의 결과로 NULL 포인터가 반환된다.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

// Polymorphic Class
class SoSimple {
public:
    virtual void ShowSimpleInfo() const {
        cout << "SoSimple Base Class" << endl;
    }
};

class SoComplex : public SoSimple {
public:
    virtual void ShowSimpleInfo() const override {
        cout << "SoComplex Derived Class" << endl;
    }
};

int main(int argc, const char * argv[]) {
    SoSimple* simPtr = new SoSimple;
    SoComplex* comPtr = dynamic_cast<SoComplex*>(simPtr);
    if(comPtr == nullptr){
        cout << "형 변환 실패" << endl;
    }
    else{
        comPtr->ShowSimpleInfo();
    }

    return 0;
}

/* 결과

형 변환 실패

*/

안정적이지 못한 형 변환을 시도하고 있다. 그리고 이러한 경우 dynamic_cast 연산자는 NULL을 반환한다.

이렇듯 dynamic_cast는 안정적인 형 변환을 보장한다. 특히 컴파일 시간이 아닌 실행 시간에(프로그램이 실행중인 동안에) 안전성을 검사하도록 컴파일러가 바이너리 코드를 생성한다.

물론 이로 인해서 실행속도는 늦어지지만, 그만큼 안정적인 형 변환이 가능한 것이다. 그리고 이러한 특성 때문에 연산자의 이름이 dynamic으로 시작하는 것이다.

그렇다면 static_cast 연산자는 어떠할까? 이 연산자는 안정성을 보장하지 않는다. 컴파일러는 무조건 형 변환이 되도록 바이너리 코드를 생성하기 때문에, 그로 인한 실행의 결과는 전적으로 프로그래머가 책임져야 한다. 그래서 이러한 특성 때문에(실행중인 동안에 안전성 검사를 진행하지 않는 특성 때문에) 연산자의 이름이 static으로 시작하는 것이다.

물론 실행속도는 빠르다. 실행 시간에 안전성 검사를 별도로 진행하지 않기 때문이다.


bad_cast 예외처리

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

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

// Polymorphic Class
class SoSimple {
public:
    virtual void ShowSimpleInfo() const {
        cout << "SoSimple Base Class" << endl;
    }
};

class SoComplex : public SoSimple {
public:
    virtual void ShowSimpleInfo() const override {
        cout << "SoComplex Derived Class" << endl;
    }
};

int main(int argc, const char * argv[]) {
    SoSimple simObj;
    SoSimple& ref = simObj;
    
    try{
        SoComplex& comRef = dynamic_cast<SoComplex&>(ref);
        comRef.ShowSimpleInfo();
    }
    catch(std::bad_cast& expn){
        cout << expn.what() << endl;
    }

    return 0;
}

/* 결과

std::bad_cast

*/

참조자 ref가 실제 참조하는 대상이 SoSimple 객체이기 때문에 SoComplex 참조형으로의 형 변환은 안전하지 못하다. 그리고 참조자를 대상으로는 NULL을 반환할 수 없기 때문에 이러한 상황에서는 bad_cast 예외가 발생한다.

위의 예제에서 보이듯이, 참조형을 대상으로 dynamic_cats 연산을 진행할 경우에는 bad_cast 예외가 발생할 수 있기 때문에 반드시 이에 대한 예외처리를 해야 한다.

Comments