타입 변환 4: 포인터

 

인프런 Rookiss님의 'Part1: C++ 프로그래밍 입문' 강의를 기반으로 정리한 필기입니다. 
😎[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part1: C++ 프로그래밍 입문 강의 들으러 가기!

 

 


 

공통 부분

 

#include <iostream>
using namespace std;
// 타입 변환 (포인터)
class Knight{
public:
int _hp = 0;
};
class Item{
public:
Item() { cout << "Item()" << endl; }
Item(int itemType) : _itemType(itemType) { }
Item(const Item& item) { cout << "Item(const Item&)" << endl; }
~Item() { cout << "~Item()" << endl; }
public:
int _itemType = 0;
int _itemDbld = 0;
char _cummy[4096] = {}; // 이런 저런 정보들로 인해 비대해진 아이템 클래스를 나타내기 위해 표시
};
enum ItemType
{
IT_WEAPON = 1,
IT_ARMOR =2,
};
class Weapon : public Item
{
public:
Weapon() : Item(IT_WEAPON)
{
_itemType = 1;
cout << "Weapon()" << endl;
}
~Weapon()
{
cout << "~Weapon()" << endl;
}
public:
int _damage = 0;
};
class Armor : public Item{
public:
Armor() : Item(IT_ARMOR) { cout << "Armor()" << endl; }
~Armor() { cout << "~Armor()" << endl; }
public:
int _defence = 0;
};

 

 


 

연관성이 없는 클래스 사이의 포인터 변환 테스트

 

int main()
{
// Stack [ 주소 ] -> Heap [ _hp(4) ]
Knight* knight = new Knight();
// 암시적으로는 NO
// 명시적으로는 OK
// Stack [ 주소 ]
Item* item = (Item*)knight;
item->_itemType = 2; // 4byte로 유효한 정보를 건드리기 때문에 정상적으로 변경된다.
item->_itemDbld = 1; // 데이터의 유효 범위를 벋어나서 문제가 된다.
delete knight;
}

 

delete knight; 하지 않는 경우, 컴파일은 통과되지만  item->_itemDbld = 1;가 문제가 된 상태로 넘어갈 수 있다.

이 경우, 추후에 나비효과가 되어 돌아올 수 있다.


 

 

다운 캐스트: 부모 -> 자식 변환 (Base class -> Derived class)

 

부모 -> 자식 변환은 명시적으로 사용해야 통과된다.

int main()
{
Item* item = new Item();
// [ [ Item ] ]
// [ _damage ]
Weapon* weapon = (Weapon*)item; // 위험한 상황, 잘못된 코드
Weapon->_damage = 10; // 잘못된 메모리를 건드림, 잘못된 코드
delete item;
}

Object Slicing 을 조심해야 한다.

 

메모리의 크기가 큰 것에서 작은 것으로 변환하는 경우, 엉뚱한 메모리를 건드릴 수 있다. 

위의 경우, item은 weapon보다 메모리의 크기가 크다. (*Weapon)item과 같이 명시적 변화를 하면 엉뚱한 메모리를 건드릴 수 있다.

 

명시적으로 타입 변환할 때는 항상 항상 조심해야 한다!


 

 

자식 -> 부모 변환 테스트

 

int main()
{
// [ [ Item ] ]
// [ _damage ]
Weapon* weapon = new Weapon();
// 암시적으로도 된다!
Item* item = weapon; // Item* item = (item*) weapon; // (item*) 부분이 생략되어있다
delete weapon;
}

 


 

조심할 사항들

 

명시적으로 타입 변환할 때는 항상 항상 조심해야 한다!
암시적으로 될 때는 안전하다?

 -> 평생 명시적으로 타입 변환(캐스팅)은 안 하면 되는거 아닌가?

int main()
{
Item* inventory[20] = {};
srand((unsigned int)time(nullptr));
for (int i = 0; i < 20; i++)
{
int randValue = rand() % 2; // 0~1
switch (randValue)
{
case 0:
inventory[i] = new Weapon();
break;
case 1:
inventory[i] = new Armor();
break;
}
}
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 : " << weapon->_damage << endl;
}
}
}

 

실행화면


 

 

 

 

 

int main()
{
for (int i = 0; i < 20; i++)
{
Item* item = inventory[i];
if (item == nullptr)
continue;
//** Virtual 함수 변경으로 delete item; 하나로 해결
if (item->_itemType == IT_WEAPON)
{
Weapon* weapon = (Weapon*)item;
delete weapon;
}
else
{
Armor* armor = (Armor*)item;
delete armor;
}
delete item;
}
// [결론]
// - 포인터 vs 일반 타입 : 차이를 이해하자
// - 포인터 사이의 타입 변환(캐스팅)을 할 때는 매우 매우 조심해야 한다!
// - 부모-자식 관계에서 부모 클래스의 소멸자에는 까먹지 말고 virtual을 붙이자!!! // 코딩 테스트와 면접 단골문제!
return 0;
}

 

 

포인터 vs 일반 타입 : 차이를 이해하자

- 포인터 사이의 타입 변환(캐스팅)을 할 때는 매우 매우 조심해야 한다!
- 부모-자식 관계에서 부모 클래스의 소멸자에는 까먹지 말고 virtual을 붙이자!!!  

 


 

 

전체 코드

 

더보기
#include <iostream>
using namespace std;
// 타입 변환 (포인터)
class Knight
{
public:
int _hp = 0;
};
class Item
{
public:
Item()
{
cout << "Item()" << endl;
}
Item(int itemType) : _itemType(itemType)
{
}
Item(const Item& item)
{
cout << "Item(const Item&)" << endl;
}
~Item()
{
cout << "~Item()" << endl;
}
public:
int _itemType = 0;
int _itemDbld = 0;
char _cummy[4096] = {}; // 이런 저런 정보들로 인해 비대해진 아이템 클래스를 나타내기 위해 표시
};
enum ItemType
{
IT_WEAPON = 1,
IT_ARMOR =2,
};
class Weapon : public Item
{
public:
Weapon() : Item(IT_WEAPON)
{
_itemType = 1;
cout << "Weapon()" << endl;
}
~Weapon()
{
cout << "~Weapon()" << endl;
}
public:
int _damage = 0;
};
class Armor : public Item
{
public:
Armor() : Item(IT_ARMOR)
{
cout << "Armor()" << endl;
}
~Armor()
{
cout << "~Armor()" << endl;
}
public:
int _defence = 0;
};
int main()
{
// 연관성이 없는 클래스 사이의 포인터 변환 테스트
{
// Stack [ 주소 ] -> Heap [ _hp(4) ]
Knight* knight = new Knight();
// 암시적으로는 NO
// 명시적으로는 OK
// Stack [ 주소 ]
// tem* item = (Item*)knight;
// item->_itemType = 2;
// item->_itemDbld = 1;
delete knight;
}
// 부모 -> 자식 변환 테스트
{
Item* item = new Item();
// [ [ Item ] ]
// [ _damage ]
Weapon* weapon = (Weapon*)item; // 위험한 상황, 잘못된 코드
Weapon->_damage = 10; // 잘못된 메모리를 건드림, 잘못된 코드
delete item;
}
// 자식 -> 부모 변환 테스트
{
// [ [ Item ] ]
// [ _damage ]
Weapon* weapon = new Weapon();
// 암시적으로도 된다!
Item* item = weapon; // Item* item = (item*) weapon; // (item*) 부분이 생략되어있다
delete weapon;
}
// 명시적으로 타입 변환할 때는 항상 항상 조심해야 한다!
// 암시적으로 될 때는 안전하다?
// -> 평생 명시적으로 타입 변환(캐스팅)은 안 하면 되는거 아닌가?
Item* inventory[20] = {};
srand((unsigned int)time(nullptr));
for (int i = 0; i < 20; i++)
{
int randValue = rand() % 2; // 0~1
switch (randValue)
{
case 0:
inventory[i] = new Weapon();
break;
case 1:
inventory[i] = new Armor();
break;
}
}
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 : " << weapon->_damage << endl;
}
}
return 0;
}