본문 바로가기

개발/내일배움캠프

C++ 인벤토리, 제작시스템 구현해보기 3

https://zerosik00.tistory.com/57

 

C++ 인벤토리, 제작시스템 구현해보기 2

https://zerosik00.tistory.com/56 C++ 인벤토리, 제작시스템 구현해보기 1목표객체 지향 설계방식과 SOLID원칙을 기반으로 하여.C++ CLI환경에서 간단한 게임 시스템을 구현해보기.그중에서도 아이템과 제작

zerosik00.tistory.com

 

저번에 만든 클래스들의 기능을 구현해보았다.

 

우선 메인함수에서 이 클래스들을 이용해 만들 기능들은

  • 아이템과 레시피 추가 및 조회
  • 아이템 제작
  • 아이템 검색(아이디, 이름을 통한 검색이나 레시피 등

관심이있다면 구현부는 글 마지막에 github링크를 참고바란다.

 

 

 

기능구현을 하다보니 이전 설계로는 부족한것들이 많았고, c++에 사용에 대한 미숙으로 const나 값전달방식, 포인터 사용등에 이슈가 꽤나 많았는데, 이부분들이 꽤 개선되었다.

 

하나씩 살펴보자면

//Inventory.h
#pragma once
#include <map>
#include "ItemDatabase.h"
class Inventory {
private:
	int maxStack = 999;
	std::map<int, int> stock;
public:
	Inventory(){}
	bool addItem(int itemId, int count);
	bool consumeItem(int itemID, int count);
	bool enoughItem(int itemID, int count);
	const std::map<int, int>& getStock() const;
};

인벤토리의 getCount 함수를 삭제했다. 조회는 getStock으로 충분하고,

개수가 충분한지 확인하는것은 enoughItem함수가 동일한 기능을 한다.

 

두번째는 ItemDatabase.h

//ItemDatabase.h
#pragma once
#include <map>
#include "Item.h"

class ItemDatabase
{
private:
	std::map<int, Item> itemDatabase;
	static ItemDatabase* instance;
	void InitializeItemDatabase();
	ItemDatabase() { InitializeItemDatabase(); }
public:
	ItemDatabase(const ItemDatabase&) = delete;
	ItemDatabase& operator=(const ItemDatabase&) = delete;
	static ItemDatabase* getInstance() {
		if (instance == nullptr) {
			instance = new ItemDatabase();
		}
		return instance;
	}
	std::map<int, Item> getAllItemInfo();
	// 추가되거나 이미 존재하는 아이템의 id를 반환.
	int tryAddCustomItem(const std::string& name, const std::string& desc);
	const Item* getItemById(const int id);
	const bool isItemExistById(const int id);
	const int isItemExistByName(const std::string& name);
};

사용자는 일반적으로 id가 아닌 아이템의 이름으로 접근하게 될것이다.

이점을 망각하고 이전에는 id를 이용한 조회밖에 없었는데 이름을 통한 조회가 추가되었다.

그리고 임시 기능으로 tryAddCustomItem 함수를 추가하여 사용자가 아이템을 추가할 수 있도록 지원한다.

 

 

마지막은 CraftingManager.h

//CraftingManager.h
#pragma once
#include "ItemDatabase.h"
#include "CraftRecipe.h"
#include "Inventory.h"
#include <map>
#include <vector>

class CraftingManager
{
private:
	std::vector<CraftRecipe> craftRecipes;
	ItemDatabase* db;
	void InitializeRecipes();
public:
	CraftingManager(){
		db = ItemDatabase::getInstance();
		InitializeRecipes();
	}
	const int CustomCraft(const std::map<int, int>& items);
	const int Craft(int ItemID, Inventory& inventory);

	const bool addCraftRecipe(const std::map<int, int>& items, const int ResultItemId);
	const CraftRecipe* getRecipeById(int id);
	const std::vector<int> getItemUsingIngredient(const int ingredientId) const;
	const std::vector<int> getCraftableItems(Inventory& inventory) const;
	const std::vector<CraftRecipe>& getAllRecipes();
};

특정 아이템의 제작법을 가져오는 getRecipeById,

직접 레시피를 추가하는 addCraftRecipe

두가지가 추가되었다.

 

단순한 함수 추가/제거외에 c++문법사용이 조금 개선되었는데

const의 위치나 값전달( & 이용한 전달)사용이 변경되었다.

 

const에 대해 간단히 설명하다면

기본적으로 const는 상수로 만드는것이고,

const func() : 함수의 앞에 const가 오면 반환값을 변경 불가능,

func() const : 함수의 뒤에 const가 가면 해당 함수 내에서는 클래스 멤버 변수를 상수화하여 변경할 수 없으며

func(const T param) : 해당함수에서 받아온 const파라미터를 상수화하여 변경할 수 없다. 이는 값전달 또는 포인터 전달시에 위험성을 잘못된 변경을 막을 수 있다.

이를 잘 활용하면 값전달을 효율적으로 하면서 수정에 대한 위험을 줄일 수 있다.

 

당연히도 void는 const를 붙일 필요가 없는데 저번에는 마구잡이로 사용하여 몇개 쓸모없는 const void함수도 있었다...

 

함수파라미터에는 값전달 & 를 쓸 수 있는데, 메모리를 적게 차지하는 int등 기본 변수형에는 굳이 사용할 필요가 없다고 한다.

 

 

 

 

그리고 이번에 CraftingManager.h의 getRecipeById함수에서 포인터를 이용했는데, 

원래는 포인터를 잘 몰라서 값 전달을 하려고 시도하였었다.

const CraftRecipe& CraftingManager::getRecipeById(int id)
{
	for (CraftRecipe rcp : craftRecipes) {
		if (rcp.getItemId() == id)
			return rcp;
	}
	return CraftRecipe();
}

처음 작성한 getRecipeById함수인데 멤버 변수인 craftRecipes 벡터로부터 특정 레시피를 찾아 반환하도록 되어잇다.

반환은 const T&를 이용하여 값 전달을 하도록 해두었다.

 

그런데 이렇게 하니 문제가 호출한 부분에서 받은 값을 다른 함수로 전달하려고 하니 값자기 값이 사라지는(접근불가)것이다!

const CraftRecipe& cr = craftManager.getRecipeById(result);
printRecipe(itemDB, cr);

문제의 코드조각인데, 찾은 레시피 변수 cr을 printRecipe함수로 전달하려하니 사라지는것이다.

왜 그런가 하고 찾아보니 포인터를 사용하지도 않았는데 Dangling Pointer 이슈가 발생하는것이다. 안쓰면 문제없는줄알았는데!

 

const CraftRecipe& CraftingManager::getRecipeById(int id)
{
	for (CraftRecipe rcp : craftRecipes) {// <- rcp는 vector인 craftRecipes를 순회하며 복사한 복사본!
		if (rcp.getItemId() == id)
			return rcp;//<-반환이 값전달방식이라 rcp는 반환은되나, 호출한 위치를 벗어나면 바로 파괴됨!
	}
	return CraftRecipe();// <- 실패시 반환할 임시객체는 return하자마자 함수가 종료되며 파괴되는것!
}

다시 getRecipeById로 돌아가서 보면 위의 주석과 같은 상황이다

지역변수의 수명은 그 함수에 한하기 때문에, 이 변수들이 파괴되는건 당연한 상황이었던것

 

꼴에 반환값을 값전달로 한탓에 호출한 위치에서 읽기는 됬었는데, 이 값을 다른 함수에 전달하니 바로 접근불가가 되버리더라.

 

이탓에 변수와 씨름하고 웹서핑을 하고나서야 댕글링포인터 이슈인것을 알고 포인터로 변경하였다.

최종수정본은 아래와같다.

const CraftRecipe* CraftingManager::getRecipeById(int id)
{
	for (const auto& rcp : craftRecipes) {
		if (rcp.getItemId() == id)
			return &rcp;
	}
	return nullptr;
}

반환값은 CraftRecipe* 포인터로 제대로 값전달을 하였으며, 실패시에 반환은 nullptr로 수정되었다.

이 덕에 반환값을 제대로 활용할 수 있으며, 실패시에 대한 처리도 더 명확해졌다!

 

그리고 드는 생각은 지금 함수들.... 값 사용처를 확인해보고 싹다 고쳐야하겟구나 라는 생각이더라.

C#과 파이썬에 익숙해서 무서운 포인터 대신 값전달을 계속해서 쓰고있었는데, 

다시 생각해보니 작성해둔 모든 값전달을 이용하고있는 함수들이 이 이슈를 터트릴 수 있다는 뜻이 되니까...

 

우선은 필요한 부분에 포인터를 이용해 미연에 같은 문제를 발생할 수 있는 부분을 개선하도록 해야겟다.

 

다음 목표는 함수에 포인터를 이용한 개선일수도있고,

추상화 적용 및 기능 확장일수도...

 

 

 

부족한 코드에 관심이있다면...

https://github.com/Zerosik/nbc_cpp_project4

 

GitHub - Zerosik/nbc_cpp_project4: 내일배움캠프 cpp 과제4

내일배움캠프 cpp 과제4. Contribute to Zerosik/nbc_cpp_project4 development by creating an account on GitHub.

github.com