캐스팅 4총사

CJB_ny·2022년 8월 17일
0

C++ 정리

목록 보기
57/95
post-thumbnail

캐스팅 (타입변환)

  • static_cast

  • adynamic_cast

  • const_cast

  • reinterpret_cast

이 4가지로 다 캐스팅한다

(int)이것은 C에서 넘어온 고전적인 캐스팅 방식

C++로 작업을 할 때는 이런 (int) c스타일은 자제하고

4가지중 하나를 선택해서 캐스팅 해야한다.

static_cast

타입 원칙에 비춰볼 때 상식적인 캐스팅만 허용해준다.
단, 안정성은 보장 못함.

1) int <-> float

ex) 3 -> 3.0f, 3.5 -> 3 이런거

2) 상속관계의 클래스 경우

Player -> Knight (다운캐스팅)

실습

int hp = 100;
int maxHp = 200;
float ratio = hp / maxHp; => 정수 / 정수 라 0이 나옴.

주의!

그래서 둘중 하나를

float ratio = (float)hp / maxHp;
이렇게 변환해줬음. float 가 우선순위가 높기 대문에

실수 / 실수 => 실수 나옴.

근데 이것은 C스타일이고 C++로하자면은

float ratio = static_cast<float>(hp) / maxHp;

이렇게된다.

상속 관계의 경우

Player는 Player이지 Knight가 아니기 때문에

p를 Knight로 변환한다는 것 자체가 위험하다.

현재는 원본은 Knight이고 들고있는거만 Player포인터 이다.

이럴경우에는 그냥 캐스팅을 해도 안전하기는 함.

이럴경우 사용할 수 있는 캐스팅이

이렇게 (Knight*)로 캐스팅해줌.

자식 -> 부모의 경우 아무 문제가 없다

(아무 문제가 없다라고 하기보다는 부모의 포인터로 변환했으니까 크기가 더 작은 걸로 변환했으니까 -> Knight의 멤버 변수에 접근하면 위험함. 안되는것은 아니지만 엉뚱한 값을 건드릴 위험이 있다.)

그래서

이렇게 가능한게

static_cast<Player*>(k); 이렇게 대체가 가능함.

부모 -> 자식으로 다운 캐스팅한것을 명시적으로 해줄 수 있음.

아까 위에서 안정성은 보장못한다는 말이 뭐냐하면은

knight와 Archer의 설계가 완전히 달라서(크기도 다르고)

k1->에 접근해서 이상한 값을 수정할 수 도 있는 것이다.

엉뚱한 메모리를 고치는 상황 발생.

dynamic_cast

static_cast의 단점을 조금 보완해줌.
상속 관계에서의 안전 형변환.

dynamic_cast는 "RTTI"를 사용한다.

"RunTime Type Informatiom" => "다형성"을 활용하는 방식.

"다형성" => virtual 함수를 부모 클래스에 만들때 -> .vftable 생성이 되는데 객체의 맨앞에 주소에 표짓말 처럼 박힌다.

이것을 유식하게 "RTTI"라고 부른다.

"실시간으로 코드가 동작할 대 타입을 알 수 있는 것"

따라서 dynamic_cast를 사용하고싶다면 virtual함수가 반드시 하나라도 등장을 해야한다.

k2라는 실제 객체가 무엇인지 가상함수 테이블을 참조를 하여 나태날 수 있다.

정리

virtual함수를 하나라도 만들면, 객체의 실제 메모리에 가상함수 테이블(vftable) 주소가 기입이 된다.

이것을 이용해서 실제로 "캐스팅"(dynamic_cast)할 때,

이렇게 무작정 캐스팅을 하지않고

맞는 타입인지 체크를 한뒤에 캐스팅을 해준다는 차이점이 있다.

만약, 잘못된 타입으로 캐스팅을 했다면, nullptr로 반환을 한다 ❗❗

=> 먼말?

만약 원본이 Archer이고 이것을 업캐스팅을 통해서 Player* p가 들고있다고 하자.

이상태에서 Knight k1 = static_cast<Knight>(p); 를 해주게 되면

다른 메모리를 오염시키는 문제가 발생한다. (k1-> 값 수정을 한다면)

반면 dynamic_cast의 경우 RTTI를 사용해서

Knight k2 = dynamic_cast<Knight>(p);

p가 Knight인지 아닌지를 가상함수 테이블을 통해 체크를 해서

맞다면 캐스팅을 해주고 아니라면 nullptr을 반환한다는 것이다.

가상함수 테이블 살펴본것

동적할당으로 Knight객체 3개를 만들었을때 당연히 메모리에 올라가는 주소는 각기 다 다르고

Player의 소멸자 부분에 virtual을 붙여주었기 때문에

메모리에 올라갈때 vftable을 달고 올라간다.

int _hp = a;이다 (10)

밑줄친 부분이 가상함수 테아블 주소인데

이게 k3, k4, k5다 똑같다.

이것으로 Player의 소멸자인지 Knight의 소멸자인지를 구분하여 호출하는 것이다.

dynamic_cast

를 사용하여 맞는 타입으로 캐스팅했는지 확인하는데 유용하다.

그런데 상속 관계에서는 그러면 dynamic_cast가 안전하고 static_cast는 위험이 조금있으니까

다이나믹만 써야지? => 이건 또 안됨.

dynamic_cast의 경우 당연히 static_cast보다 조금더 느릭 동작할 수 빆에 없다. (원본을 체크를 하니까)

만약 조금 더 빠르게 동작 시키기 위해서는 ❓

타입 변환 3 : 포인터 에서

#include <iostream>
using namespace std;

// 타입 변환 3 : 포인터

class Knight
{
public:
	int _hp = 0;
};

class Item
{
public:
	Item()
	{
		cout << "Item 기본 생성자 호출!" << endl;
	}
	Item(int itemType)
		:
		_itemType(itemType)
	{
		cout << "Item itemType 생성자 호출!" << endl;
	}
	Item(const Item& item)
	{
		cout << "Item 복사 생성자 호출!" << endl;
	}
	virtual ~Item()
	{
		cout << "Item 소멸자 호출!" << endl;
	}

public:
	int _itemType = 3;
	int _itemDbid = 3;

	char _dummy[4096] = {};
};

enum ItemType
{
	IT_Weapon = 1,
	IT_Armor = 2,
};

class Weapon : public Item
{
public:
	Weapon()
		:
		Item(IT_Weapon)
	{
		cout << "Weapon 생성자 호출!" << endl;
	}
	virtual ~Weapon()
	{
		cout << "Weapon 소멸자 호출!" << endl;
	}
public:
	int _damage = 10;

};

class Armor : public Item
{
public:
	Armor()
		:
		Item(IT_Armor)
	{
		cout << "Armor 생성자 호출!" << endl;
	}
	virtual ~Armor()
	{
		cout << "Armor 소멸자 호출!" << endl;
	}
public :
	int _defense = 10;
};

int main()
{	
	Item* inventory[20] = {};

	srand((unsigned)time(nullptr));

	for (int i = 0; i < 20; ++i)
	{
		int result = rand() % 2; // 0~1

		switch (result)
		{
		case 0:
			inventory[i] = new Weapon();
			break;
		case 1:
			inventory[i] = new Armor();
			break;
		default:
			cout << "Error!" << endl;
			break;
		}
	}

	cout << sizeof(Item) << endl;
	cout << sizeof(Weapon) << endl;

	for (int i = 0; i < 20; ++i)
	{
		Item* item = inventory[i];

		if (item == nullptr)
			continue;
		
		if (item->_itemType == IT_Weapon)
		{
			Weapon* weapon = (Weapon*)item;
			cout << weapon->_damage << endl;
		}
	}

	for (int i = 0; i < 20; ++i)
	{
		Item* item = inventory[i];

		if (item == nullptr)
			continue;

		delete item;
	}
	return 0;
}

이런식으로 Item을 상속받는 Weapon에 기본생성자의

선처리 영역에서 인자를 하나만 받는 Item 생성자를 호출하여 ItemType을 지정하고나서

if문 안에서 ItemType에 따라서

Weapon weapon = (Weapon)itme;이렇게 캐스팅 해주었는데 이부분을 이제

	for (int i = 0; i < 20; ++i)
	{
		Item* item = inventory[i];

		if (item == nullptr)
			continue;
		
		if (item->_itemType == IT_Weapon)
		{
			Weapon* weapon = static_cast<Weapon*>(item);
			cout << weapon->_damage << endl;
		}
	}

이렇게 바꾸면 조금 더 빠를 수 있다는 것이다.

const_cast

const를 붙이거나 때거나~

누군가가 이런식으로 함수를 만들어서 이것을 수정할 수 없고 그대로 가져다가 써야하는 상황일 경우

"Rookiss"라는 문자열의 데이터 타입은

const char* 이다 보니까 인자로 넘겨줄 수 없다.

그래서 const를 빼기 위해서 (char*)로 캐스팅가능하지만

실수할 여지가 있기 때문에

const_cast<char*>("Rookiss")

이렇게 넘겨줄 수 있다.

하지만 거의 대부분의 경우에서 이것을 사용할 경우는 진짜 없다.

면접을 위해서 공부를 하자.

reinterpret_cast

가장 위험하고 강력한 형태의 캐스팅.
re-interpret : 다시-간주하다/생각하다.

포인터랑 전혀 관계없는 다른 타입 변환 등

이렇게 C스타일로 형변환 가능하지만

전혀 엉뚱한 값으로 캐스팅 할 때 사용이 가능하다.

포인터 <-> 정수 가능함.

또한

Dog라는 아무런 연관성이 없는 클래스들 끼리도 변환이 가능하다.

Dog*에다가 k2를 넣어주면 당연히 말이 안됨.

static_cast를 사용하더라도 통과가 안된다.

주의 ❗

static_cast, dynamic_cast는 정말로 확실하게 부모-자식 상관관계 있을 때나 사용할 수 있지(static_cast는 값 캐스팅 됨)

언제 어디서나 막 사용할 수 있는 문법은 아니다.

그래서 이런 생뚱맞은 변환을 하고 싶을 경우

이렇게 reinterpret_cast 사용한다.

그러면 시발 이럴 경우가 없는데 왜 사용을 하냐?

=> C의 경우 malloc같은 경우에 사용할 수 있다.

malloc의 반환 타입이 void* 인데 이것을 우리가

원하는 형태로 캐스팅 해주어야한다.

void* p = malloc(1000);
// 어떤 의미로 사용할지는 모르겠으니까 니가 알아서 사용해줘 이느낌 이였다.

Dog* dog = p; // 기본적으로 통과가 안된다.

// 이럴때 C스타일의 캐스팅을 사용해서

Dog* dog = (Dog*)p; // 이렇게 형변환 해주었음. ( 실수 할 여지 있음)

// 근데 이제는 C++ 문법을 사용해야한다 

Dog* dog = reinterpret_cast<Dog*>(p);
// 이렇게 사용가능함.

90% -> static_cast

8% -> dynamic_cast

1% -> const_cast

1% -> reinterpret_cast

활용빈도가 이럼.

profile
https://cjbworld.tistory.com/ <- 이사중

0개의 댓글