티스토리 뷰

주의 사항!

  • 이 글은 제가 직접 공부하는 중에 작성되고 있습니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

C++ 진영에서는 C 스타일의 형 변환 연산자를 가리켜 '오래된 C 스타일 형 변환 연산자'라고 부르기도 합니다. C 스타일의 형 변환 연산자는 C언어와의 호환성을 위해서 존재할 뿐, C++에서는 새로운 형 변환 연산자와 규칙을 제공하고 있습니다.

 

모기를 잡으려면 모기약을 뿌려야 하고, 바퀴벌레를 잡으려면 바퀴벌레 약을 뿌려야 합니다. 모기나 바퀴벌레를 잡자고 사람까지 잡을 수 있는 독한 약을 써서는 안 됩니다. 그런데 C++에 있어서 C언어의 형 변환 연산자는 사람까지 잡는 독한 약에 비유됩니다. 그만큼 강력해서 형 변환하지 못하는 대상이 없기 때문입니다. 그래서 아래의 예제에서 보이는 실수를 해도 컴파일러는 이를 잡아내지 못합니다. 다음의 예제를 살펴보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <new>
using namespace std;

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()
	{
		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

*/

 

위 예제에서 다음과 같은 문장을 보겠습니다.

Car* pcar2 = new Car(120);
Truck* ptruck2 = (Truck*)pcar2;    //문제가 바로 보이는 형 변환
ptruck2->ShowTruckState();

 

 

해당 문장은 문제가 있다는 것이 바로 보입니다. Truck클래스가 Car 클래스를 상속받고 있지만, Car클래스의 객체의 자료형을 Truck으로 바꾸게 되면 자료형은 Truck이지만 Car의 멤버만 가지고 있을 뿐 Truck의 멤버는 존재하지 않기 때문에 이는 문제가 됩니다.

 

Car* pcar1 = new Truck(80, 200);
Truck* ptruck1 = (Truck*)pcar1;    //문제 없어 보이는 형 변환
ptruck1->ShowTruckState();

 

위 문장은 얼핏 봐서는 문제가 없어 보입니다. Truck클래스가 Car클래스를 상속받고 있기 때문에, Truck객체를 Car* 포인터가 가리키는 것도, Truck객체의 자료형을 Truck으로 바꾸는 것도 문제가 없습니다. 하지만 다 아시겠지만 굳이 이런 식으로 형 변환할 필요가 없습니다. 즉, 위 문장은 문제라기보다 프로그래머의 실수에 더 가깝다고 볼 수 있습니다.

 

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

  • static_cast
  • const_cast
  • dynamic_cast
  • reinterpret_cast

 

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

 

dynamic_cast 형 변환 연산자

먼저, dynamic_cast 형 변환 연산자부터 배워 보겠습니다. dynamic_cast 형 변환 연산자는 상속 관계에서의 안전한 형 변환을 가능하게 해주는 연산자로서, 다음의 형태를 갖습니다.

dynamic_cast<T>(expr)

 

<> 사이에 변환하고자 하는 자료형의 이름을 두되, 객체의 포인터 또는 참조형이 와야 합니다. 그리고 ( ) 사이에는 변환의 대상이 와야 합니다.

 

요구한 형 변환이 적절한 경우에는 형 변환된 데이터를 반환하지만, 요구한 형 변환이 적절하지 않은 경우에는 컴파일 에러가 발생합니다. 여기서 적절한 경우란 '상속 관계에 놓여 있는 두 클래스 사이에서 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환하는 경우'를 말합니다.

 

다음 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <new>
using namespace std;

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()
	{
		ShowCarState();
		cout << "화물의 무게 : " << freightWeight << endl;
	}
};

int main(void)
{
	Car* pcar1 = new Truck(80, 200);
	Truck* ptruck1 = dynamic_cast<Truck*>(pcar1);    //컴파일 에러

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

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

	return 0;
}

 

dynamic_cast는 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 때 사용합니다. 그 반대로는 형 변환을 하지 않습니다. 따라서 위 예제에서 Car* 형을 Truck* 형으로 형 변환을 시도했을 때 컴파일 에러가 발생합니다.

 

 

그런데 위 예제 main 함수의 첫 번째 형 변환 시도 문장을 다시 보겠습니다.

Car* pcar1 = new Truck(80, 200);
Truck* ptruck1 = dynamic_cast<Truck*>(pcar1);    //컴파일 에러

 

위 문장을 보면 Truck 객체의 주소를 저장하는 Car* 형 변수인 pcar1을 Truck* 형으로 형 변환하는 것을 시도합니다. 물론 dynamic_cast연산자는 이를 지원하지 않지만, 해당 형 변환 자체만 놓고 보면 안 될 이유가 없고, 또 어떤 상황에서는 이런 형 변환이 필요할지도 모르겠다는 생각도 듭니다.

 

위의 경우에 맞는 형 변환 연산자가 바로 static_cast 연산자입니다.

 

static_cast 형 변환 연산자

static_cast 연산자는 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환시켜줄 뿐만 아니라 그 반대의 경우에 대해서도 수행합니다. 즉, 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환시켜 줄 수 있습니다. 다만, 해당 형 변환으로 인해 발생하는 문제는 프로그래머가 책임져야 합니다. 여기에 더해서 static_cast 연산자는 기본 자료형 데이터 간의 형 변환도 수행합니다.

 

다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <new>
using namespace std;

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()
	{
		ShowCarState();
		cout << "화물의 무게 : " << freightWeight << endl;
	}
};

int main(void)
{
	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
화물의 무게 : 200

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

*/

 

static_cast 연산자는 기초 클래스에서 유도 클래스로의 형 변환을 가능하게 하지만 그에 대한 책임은 프로그래머가 져야 한다고 했습니다. 다음의 문장을 보면,

Car* pcar1 = new Truck(80, 200);
Truck* ptruck1 = static_cast<Truck*>(pcar1);    //컴파일 가능!
ptruck1->ShowTruckState();
cout << endl;

 

Car* 형을 Truck* 형으로 형 변환했습니다. 그리고 해당 포인터 변수가 어차피 Truck객체를 가리키고 있기 때문에 이런 형 변환으로 발생하는 문제는 아마도 없을 것입니다. 그러나 다음의 문장은 다릅니다.

Car* pcar2 = new Car(120);
Truck* ptruck2 = static_cast<Truck*>(pcar2);    //컴파일은 가능하지만...
ptruck2->ShowTruckState();

 

위 문장을 보면 앞선 문장과 똑같은 형 변환을 수행했지만, 해당 포인터 변수가 가리키는 것은 Car 객체입니다. 즉, Truck* 형 포인터 변수로 Car 객체를 가리키고 있습니다. 이런 경우에 Car객체는 Truck객체가 가지고 있는 변수들을 가지고 있지 않기 때문에 프로그래머가 코드 작성에 주의하지 않으면 실수를 범하기 쉽습니다. 따라서 실행 결과를 보면 화물의 무게가 이상한 값으로 출력됨을 확인할 수 있습니다.

 

static_cast 연산자는 기본 자료형 데이터 간 형 변환도 수행한다고 했습니다. 예를 들어 다음의 문장은,

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

 

result에 올바른 값을 대입하기 위해 다음과 같이 문장을 수정할 수 있습니다.

int num1 = 20, num2 = 3;
double result = static_cast<double>(20) / 3;

 

const_cast 형 변환 연산자

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

const_cast<T>(expr)

 

다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

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

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

int main(void)
{
	ShowString(const_cast<char*>("홍길동"));

	const int& ref1 = 100;
	const int& ref2 = 150;
	ShowAddResult(const_cast<int&>(ref1), const_cast<int&>(ref2));

	return 0;
}

/*
실행결과

홍길동
250

*/

 

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

 

하지만 const_cast 연산자를 남발하게 되면 const가 존재하는 의미와 const의 유용한 장점이 퇴색될 수 있기 때문에 const_cast 연산자의 긍정적인 측면이 잘 드러나는 경우에만 제한적으로 사용해야 합니다.

 

reinterpret_cast 형 변환 연산자

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

reinterpret_cast<T>(expr)

 

예를 들어서 다음과 같이 두 클래스가 정의되어 있다고 가정해 보겠습니다.

class SimpleCar {......};
class BestFriend {......};

 

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

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

 

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

 

dynamic_cast 형 변환의 예외 조건

앞서 dynamic_cast 연산자는 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환하지 못한다고 했습니다. 그런데 어떠한 조건만 달성되면 dynamic_cast 연산자도 기초 클래스로부터 유도 클래스로의 형 변환을 허용합니다.

 

그 조건은 '기초 클래스가 Polymorphic 클래스인 경우'입니다. polymorphic 클래스란 하나 이상의 가상 함수를 지니는 클래스를 말합니다. 다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class Car
{
private:
	int fuelGauge;
public:
	Car(int fuel) : fuelGauge(fuel) {}
	void ShowCarState() { cout << "잔여 연료량 : " << fuelGauge << endl; }
	virtual void ShowTruckState() = 0;    //순수 가상 함수
};

class Truck : public Car
{
private:
	int freightWeight;
public:
	Truck(int fuel, int weight) : Car(fuel), freightWeight(weight) {}
	void ShowTruckState()
	{
		ShowCarState();
		cout << "화물의 무게 : " << freightWeight << endl;
	}
};

int main(void)
{
	Car* pcar = new Truck(80, 200);
	Truck* ptruck = dynamic_cast<Truck*>(pcar);
	ptruck->ShowTruckState();

	return 0;
}

/*
실행결과

잔여 연료량 : 80
화물의 무게 : 200

*/

 

위 예제를 보면 Car 클래스에 ShowTruckState 함수가 순수 가상 함수로 선언되어 있습니다. 따라서 dynamic_cast 연산자를 이용해서 Car* 형을 Truck*형으로 형 변환할 수 있었습니다.

 

다만 이런 형 변환이 가능했던 이유는 형 변환 이후 Truck* 포인터가 가리키는 대상이 어차피 Truck객체였기 때문입니다. 만약 다음과 같이 문장이 수정된다면,

Car* pcar = new Car(80);
Truck* ptruck = dynamic_cast<Truck*>(pcar);
ptruck->ShowTruckState();

 

형 변환은 불가능하며 그 결과로 dynamin_cast 연산자는 NULL 포인터를 반환합니다. 즉, dynamic_cast는 매우 안정적인 형 변환을 보장합니다.

'공부 일지 > CPP 공부 일지' 카테고리의 다른 글

C++ | auto 타입과 decltype 타입  (0) 2021.08.06
C++17 STL | 구조체 형태의 바인딩  (0) 2021.08.05
C++ | 예외상황과 예외처리  (0) 2021.08.05
C++ | 템플릿(template)  (0) 2021.08.05
C++ | std::string  (0) 2021.08.03
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   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
글 보관함