자료구조의 사용선택
1. 왜 사용하는가?
2. 어떤때 어떤 자료구조를 사용해야 하는가?
이 두가지로 나뉜다.
class Item
{
};
위 클래스는 Item의 가장 기본기능을 정의하기 위해 만들어진 클래스다.
우선적으로 이름을 가지고 있어야 할거같은데 이름은 아이템에서만 사용되지않고
케릭터,스킬 등등 사용하는곳이 많다.그렇기 때문에
#include <string.h>
class NameBase
{
private:
std::string m_Name;
public:
std::string Name()
{
return m_Name;
}
void Name(const std::string& _Name)
{
m_Name = _Name;
}
};
이렇게 NameBase 클래스를 만들어서 상속해서 IS A 관계로 사용해서 설계할수 있다.
class Item : public NameBase
{
public:
Item(std::string _Name)//만들어 질때 무조건 이름을 들고있다
{
Name(_Name); //부모가 가지고 있기때문에 초기화 불가
}
};
class Weapon : public Item
{
private:
int m_Att;
};
class Armor : public Item
{
};
class Shop
{
};
아이템에는 무기와 방어구가 있고 그 아이템을 파는 상점을 만들었을때
무기상점,방어구상점을 각각 만드는건 비효율적이고 상점에서는 몇개의
물건을 파는지 알수 없다.
그렇기 때문에 vector를 사용해서 아이템을 들고있게 만들 수 있다.
class Shop
{
public:
std::vector<Item> ItemInventory;
public:
void CreateItem(std::string _Name)
{
ItemInventory.push_back(Item(_Name));
}
public:
void Render()
{
for(int i = 0; i < ItemInventory.size(); ++i)
{
std::cout << i + 1 << ". "<<ItemInventory[i].Name()<< std::endl;
}
}
};
int main()
{
Shop NewShop = Shop();
NewShop.CreateItem("철검");
NewShop.CreateItem("방패");
NewShop.CreateItem("포션");
NewShop.Render();
}
이렇게 만들면 Shop을 하나 생성해서 거기에 아이템을 추가하고 출력할 수 있다.
public:
Item* GetItem(int _Index)
{
return &ItemInventory[_Index];
}
Shop에는 vector니깐 인덱스로 아이템을 가져오는 함수를 위와같이 만들 수 있다.
여기서 Item의 포인터를 리턴하는 이유는 만약 위의 코드에서 5번의 아이템을
달라고 했을때 5번에는 아이템이 존재하지 않는데 레퍼런스로 리턴했을 경우에는
비어있다를 표현할 수 없기 때문이다.(레퍼런스는 무조건 값이 채워져 있어야한다)
public:
Item* GetItem(int _Index)
{
if(0 > _Index)
{
return nullptr;
}
if(_Index <= ItemInventory.size())
{
return nullptr;
}
return &ItemInventory[_Index];
}
이렇게 사용하기 위함이다.레퍼런스일 경우에는 비어있는 아이템을 선택했을때
해줄 수 있는 방법이 없다.
class Weapon : public Item
{
public:
Weapon(std::string _Name) : Item(_Name), m_Att(1)
{
}
public:
void Render()
{
Item::Render();//부모의 함수 호출하는 방법
std::cout << "공격력: " << m_Att << std::endl;
}
};
무기 클래스에서 생성자를 만들어주고
생성자에서 멤버이니셜라이즈 문법을 통해 부모의 생성자를 호출 할 수 있다.
위와같이 만들었을때 문제점이 있다.
메인함수에서 철검,방패,포션을 만들었는데 원하는거는 철검은 Weapon클래스로
방패는 Armor 클래스로 포션은 또 다른 클래스로 나뉘길 원하지만
위와같은 코드에서는 무조건 다 Item으로 만들어 지고 있다.
class Shop
{
public:
void CreateWeapon(std::string _Name)
{
ItemInventory.push_back(Weapon(_Name));
}
};
위와같이 CreateWeapon 함수를 만들어서 철검은 Weapon 클래스로 만들 수 있다.
그치만 여기서
int main()
{
Item* NewItem = NewShop.GetItem(0);
NewItem->Render();
}
이렇게 했을경우 NewItem에는 Weapon 클래스의 철검이 들어가니
그걸 Render 호출하면 Weapon 클래스의 Render 함수가 호출되길 원하지만
그렇게 되지 않는다. 생각해 보면은
class Shop
{
public:
std::vector<Item> ItemInventory;
};
여기서 벡터의 Item에는 값형이 들어가 있고 40byte짜리 Item 값이 들어가있다.
(부모 클래스인 NameBase의 크기가 40byte, Item 클래스는 내부에 아무것도
없기때문에 1byte니깐 총 41byte라고 생각할수 있지만 클래스에 아무것도
없을경우 부모클래스가 있으면 거기에 합쳐져서 묻어(?)갈 수 있다.)
그치만 Weapon 클래스의 크기는 48byte이다.
(NameBase의 40byte, Weapon의 4byte, Item의 4byte(바이트패딩))
그럼 위의 CreateWeapon 함수는 40byte에 48byte를 넣는다는 소리가된다.
이럴경우 축소변환되서 Weapon을 넣었지만 Item이 들어가게 된다.
애초에 값형으로 가지고있으면 담을수가 없기때문에 잘못된 코딩이다
std::vector<Item*> ItemInventory;
ItemInventory.push_back(new Item(_Name));
이렇게 포인터를 써서 주소값으로 들고있게 해줘야 한다.
그러면서 new를 사용해서 동적할당을 해준다.
그렇게되면 Heap에 48byte가 생기고 ItemInventory의 Item*가 거기에 연결된다.
그렇지만 이렇게 해도 Weapon의 Render함수가 호출되지 않는다.
Item* 이기 때문에 Item을 가리키고 있고 당연히 Item의 Render를 호출한다.
Weapon의 Render를 호출해야 한다고 알려줘야 한다.
그렇다면 어떤 방법이 있을까?
안좋은방법
class Item : public NameBase
{
public:
int m_Type;
public:
Item(std::string _Name,int _ItemType = 0) : m_Type(_ItemType)
{
Name(_Name);
}
public:
int Type()
{
return m_Type;
}
};
class Weapon : public Item
{
public:
Waepon(std::string _Name) : Item(_Name, 1), m_Att(1)
{
}
};
위의 코드처럼 Item 클래스에 m_Type 멤버변수를 추가해서 m_Type이 0이면 소모아이템
1이면 무기 2이면 방어구 이런식으로 정해주고 각 아이템의 생성자에서
타입에 맞게끔 초기화를 해주고 사용할때 Type을 리턴받아와서
타입별로 형변환을 해줘서 호출을 해주는 방법이다.
switch (NewItem->Type())
{
case 1:
Weapon* GetWeapon = (Weapon*)NewItem;
GetWeapon->Render();
break;
}
이렇게 해줄 수 있다. 이럴경우 부모가 자식형이 되는 꼴이다.
이런경우를 다운캐스팅 이라고 한다.
다운캐스팅은 사용하지 않을수록 좋다.이유는 휴먼에러이다.
코드를 치다가 실수로 m_type값을 잘못 넣어준다거나 그럴경우
원하는 방향으로 실행되지 않는다.
가상함수를 이용하는 방법
위에서 보면 CreateWeapon 함수를 이용해서 만들때는 Weapon 클래스로 만들어주고 있다.
본체는 Weapon이지만 쓸때만 Item*이기 때문이다.
(포인터는 주소값을 나타내기 때문에 무조건 8byte이라 대입되고 들어갈수 있다.)
여기서 원하는 거는 본체가 Weapon이면 Weapon의 Render가 호출되게끔 하는것이다.
이를 해결하기 위한 방법으로 가상함수가 있다.
가상함수란 c++에서 다형성을 표현하는 핵심요소이다.
class Item : public NameBase
{
public:
virtual void Render()
{
std::cout << Name() << std::endl;
}
};
부모의 함수앞에 virtual 키워드를 이용해서 가상함수로 만들 수 있다.
이렇게 가상함수로 만들어주면 이제부터 자식들이 이 함수를 똑같이 구현하고
부모의 형으로 이 함수를 호출하면 자식의 함수가 호출된다.
이렇게 가상함수를 사용하면 Item하나로 여러종류의 아이템들이 관리가 가능해진다.
배열을 안쓰고 vector를 사용한 이유도 메모리적 한계는 있지만 원하는 만큼 넣을 수 있다.
상점에는 아이템수가 고정일 수도 있지만 아닐경우도 많기 때문이다.
그렇다면 아이템 종류가 수십개라고 가정하면 그 종류마다 Create땡땡땡 함수를 만들어서
그에 맞는 종류의 아이템을 생성하는 함수를 각각 만들어 줘야 할까?보면은 생성해주는 자료형만 다르고 다 똑같다.이럴 경우 template 문법을 이용해서 해결이 가능하다.
template<typename T>
void CreateItem(std::string _Name)
{
ItemInventory.push_back(new T(_Name));
}
int main()
{
Shop NewShop = Shop();
NewShop.CreateItem<Weapon>("철검");
NewShop.CreateItem<Armor>("방패");
NewShop.CreateItem<땡땡>("땡땡");
NewShop.CreateItem<땡땡땡>("땡땡땡");
}
템플릿을 사용해서 CreateWeapon() 함수를 사용할 필요없이 하나의 함수로
여러종류의 아이템을 만드는게 가능해진다.