C++의 형 변환 연산자(메모용, 윤성우 열혈 C++ 프로그래밍 정리 CH 16)

RisingJade의 개발기록·2022년 3월 2일
0

윤성우 열혈C++ 정리

목록 보기
13/13

Chapter 16. C++의 형 변환 연산자


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

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

닭잡는데 소잡는 칼을 쓰지 말자

C언어의 형 변환 연산자는 너무나도 강력해서 변환하지 못하는 대상이 없다.
따라서 아래의 예제에서 보이는 실수를 해도 컴파일러는 이를 잡아내지 못한다.

class Car{
private:
	int fuelGauge;
public:
	Car(int fuel): fuelGauge(fuel)
    {}
    void showCarState(){ cout << feulGauge << endl;}
};
class Truck : public Car
{
private: 
	int freightWeight;
public:
	Truck(int fuel, int weight) : Car(feul), freightWeight(weight)
    {}
    void ShowTruckState()
    {
    	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();
    cout << endl;
}

위 예시에서 Truck * ptruck1=(Truck *)pcar1 이 형 변환 연산은 문제가 되지 않을 수 있다. 하지만, 기초 클래스의 포인터 형을 유도 클래스의 포인터 형으로 형 변환 하는것은 일반적인 경우의 형 변환이 아니다.

  • 따라서 이 상황에서 이것이 프로그래머의 의도인지, 아니면 실수인지 알 방법이 없다...

더욱 큰일인 문장은
Truck * ptruck2=(Truck *)pcar2 ... 이게 가능..한가...?
포인터 변수 pcar2가 가리키는 대상이 실제로는 Car객체이기 때문에 유도 클래스로의 형 변환 연산은 "당연히" 문제가 된다.

  • 하지만 C 스타일의 형 변환 연산자는 컴파일러로 하여금 이러한 일이 가능하게 한다. 무적의 형 변환 연산자이기 때문이다...
  • 따라서, ptruck2->ShowTruckState() 이런 막돼먹은 호출은 논리적으로 맞지도 않고 이 포인터가 가지고 있는 객체에는 화물의 무게를 의미하는 freightWeight가 없기 때문에 우리의 상식대로라면 에러가 나야된다.

    하지만 우리의 무적의 C 형변환 연산자는 그냥 설정도 안되어있는 freightWright에 접근하여 쓰레기값을 반환한다... 경악..

이와 같은 유형의 논란과 문제점 때문에 C++에서는 다음과 같이 총 4개의 연산자를 추가로 제공하면서 용도에 맞는 형 변환 연산자의 사용을 유도하고 있다.

  • static_cast
  • const_cast
  • dynamic_cast
  • reinterpret_cast

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

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

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

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

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

int main(void)
Car * pcar1 = new Truck(200, 3000);
//아무리 원래 객체가 Truck 포인터 객체라 하더라도 Car 포인터로 받아진 이상 다시 유도 클래스로 들어가는 것을 막는다.
Truck * ptruck1 = dynamic_cast<Truck*>(pcar1); //컴파일 에러! (기초 클래스 객체 포인터가 유도클래스에 객체포인터에 들어가는 것을 방지!
Car * pcar2 = new Car(200, 3000);
//위에것도 안됬는데 이게 될일이 없다. 컴파일 에러 발생
Truck * ptruck2 = dynamic_cast<Truck*>(pcar2); 
// 컴파일 성공! 옳게 된 형 변환이다.
Truck * ptruck3 = new Truck(200, 3000);
Car * pCar3 = dynamic_cast<Truck*>(ptruck3); 

정리하면 dynamic_cast 연산자를 사용했다는 것은 다음과 같은 뜻이다.

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

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

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

Car * pcar1 = new Truck(200, 3000);
Truck * ptruck1 = dynamic_cast<Truck*>(pcar1); // 이거 된다. 사실 원래 객체가 truck이라 프로그래머가 알고 쓸경우 큰 문제없다.
Car * pcar2 = new Car(200, 3000);
Truck * ptruck2 = static_cast<Truck*>(pcar2); // 이게 된다... 프로그래머가 잘못쓴것... 컴파일러는 이걸 변환해준다...
  • static_cast<T>(expr)

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

  • 근데 dynamic보다 static이 연산의 속도가 좀더 빠르다... 이러한 이유로 속도가 중요할땐 static을 그냥 쓰는 경우도 있다.

  • 기본 자료형 데이터간의 형 변환에도 사용된다. 이때, C언어 형 변환연산자랑 다른점은 Static_cast는

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

만을 지원하기 때문, C언어의 형 변환연산자 같이 무시무시한 변환은 안한다.
ex)

const int num =20;
int * ptr = (int*)&num;
*ptr = 30; //-> 이게 된다... const인데 값 변경이 가능해진다... 노오오올라울 따름..

const_cast: const 성향을 삭제하라!

위와 같이 const 성향을 무시하고 싶을 때가 있을때 사용한다.(근데 그럴때가...)
C++에서는 포인터와 참조자의 const 성향을 제거하는 형 변환을 목적으로, 다음의 형 변환 연산자를 제공하고 있다.

  • const_cast<T>(expr)
void ShowString(char* str){
	cout << str <<endl;
}
void ShowAddResult(int& n1, int& n2){
	cout << n1+n2 << endl;
}
int main(void){
	const char* name = "RisingJade";
    ShowString(const_cast<char*>(name));//원래대로라면 const인 name은 char* str에 들어갈 수 없지만 캐스팅을 통해 const 속성을 없애서 가능해졌다
    const int& num1 = 100;
    const int& num2 = 200;
    ShowAddresult(const_cast<int&>(num1),const_cast<int&>(num2)); // 이것도 const 속성이 없어져서 가능해진것
}

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

  • 하지만 이것 또한 const 선언의 의미가 반감되는 효과가 있으므로 긍정적인 측면이 잘 드러나는 경우에만 제한적으로 쓰자
  • voletile 성향을 제거하는데도 사용할 수 있다.

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

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

  • reinterpret_cast<T>(expr)

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

int num = 82;
int *ptr = &num;

int adr = reinterpret_cast<int>(ptr); // 주소 값을 정수로 변환
cout << "Address: " << adr << endl;//주소 값 출력
int rptr = reinterpret_cast<int*>(adr); // 정수를 다시 주소 값으로 변환
cout << " value : " << *rptr<<endl; // 주소 값에 저장된 정수 출력

reinterpret_cast를 통해 위와 같은 일을 할 수 있다.

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

기초클래스가 Polymorphic 클래스인 경우 dynamic_cast를 이용하여 기토 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 변환 가능하다!

즉, 기초클래스가 Polymorphic 클래스인 경우 굳이 static_cast를 안써도 된다는 의미이다.

  • Polymorphic 클래스란 하나 이상의 가상함수를 지니는 클래스를 뜻한다. 즉, 상속관계에 놓여있는 두클래스 사이에서, 기초 클래스에 가상 함수가 하나 이상 존재한다면, dynamic_cast를 쓸 수 있다.
  • 이때, 유도 클래스 객체 포인터로 캐스팅 할 기초 클래스의 객체 포인터가 실제 가리키는 것이 유도 클래스 객체가 아니면 NULL을 반환한다.
class SoSimple{
public:
	virtual void ShowSimpleInfo(){
    	cout << "SoSimple" << endl;
    }
};

class SoComplex : publiv SoSimple{
public:
	void ShowSimpleInfo(){
    	cout << "SoComplex" << endl;
    }
};

int main(void){
	SoSimple * simPtr = new SoComplex;
    SoComplex * comPtr = dynamic_cast<SoComplex*>(simPtr);//원래대로면 안되야 하지만 SoSimple(기초 클래스)가 Polymorphic이라 가능!
    comPtr->ShowSimpleInfo();
    return 0;
}
_____
console:
SoComplex

여기서

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

이 부분이 가능했던것은 simPtr이 가리키는 객체가 SoComplex 객체이기 때문이다. 즉, 포인터 변수 simPtr이 가리키는 객체를 SoComplex형 포인터 변수 comPtr이 함께 가리켜도 문제되지 않기 때문에 성공한 것이다.
만약 이부분을

SoSimple * simPtr = new SoSimple; // 변경된 부분
SoComplex * comPtr = dynamic_cast<SoComplex*>(simPtr);

이와 같이 바꾸면 형 변환의 결과로 NULL포인터가 반환된다.

bad_cast

프로그래머가 정의하지 않아도 발생하는 예외도 있다. 그런 유형의 예외 중 하나로 형 변환시 발생하는 bad_cast가 있다.

class SoSimple{
public:
	virtual void ShowSimpleInfo(){
    	cout << "SoSimple" << endl;
    }
};

class SoComplex : publiv SoSimple{
public:
	void ShowSimpleInfo(){
    	cout << "SoComplex" << endl;
    }
};
int main(void){
	SoSimple simObj;
    SoSimple& ref = simObj;
    
    try{
    //ref가 실제 참조하는 것은 SoSimple이다!! 이럴경우엔 polymorphic클래스라도 안된다.
    	SoComplex& comRef=dynamic_cast<SoComplex&>(ref);// 예외 발생!
        comRef.ShowSimpleInfo(); // 예외 발생으로 인해 실행 안됨
    }
    catch(bad_cast expt){
    	cout << expt.what() << endl;
    }
    return 0;
}

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

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

profile
언제나 감사하며 살자!

0개의 댓글