본문 바로가기

숙제

3번 과제 복습(인벤토리 시스템 구현)

과제 목표가 동적 메모리 할당과 해제를 직접 관리하며 템플릿 클래스를 활용하여 벡터의 핵심 동작 원리를 이해하기 였는데 이번 복습에서는 벡터를 활용해서도 구현해 볼 예정이다.

필수 기능

`Inventory<T>` 클래스 구현
`Item` 뿐만 아니라, `Weapon`, `Potion` 등 어떤 타입의 객체든 저장할 수 있도록 템플릿 클래스로 작성합니다.

클래스 이름: `Inventory`
템플릿: `template <typename T>`를 사용하여 어떤 타입(`T`)의 데이터든 처리할 수 있도록 합니다.

멤버 변수 (private)
`T* pItems_`: 아이템 객체들을 저장할 동적 배열을 가리키는 포인터입니다. `new T[]`를 통해 메모리를 할당받습니다.
`int capacity_`: 인벤토리가 최대로 저장할 수 있는 공간의 크기입니다.
`int size_`: 현재 인벤토리에 저장된 아이템의 실제 개수입니다.

생성자, 소멸자
생성자: `Inventory(int capacity = 10)`
인벤토리 객체가 생성될 때 호출됩니다.
매개변수로 인벤토리의 용량(`capacity`)을 받으며, 값을 전달하지 않으면 기본값으로 10이 설정됩니다.
안정성을 위해, 만약 0 이하의 `capacity` 값이 들어오면 최소 용량을 1로 보정합니다.
`new T[capacity_]`를 실행하여 아이템을 저장할 메모리 공간을 힙(Heap)에 할당합니다.
소멸자: `~Inventory()`
인벤토리 객체가 소멸될 때 (예: `main` 함수 종료 시) 자동으로 호출됩니다.
`delete[] pItems_`를 실행하여 생성자에서 할당했던 메모리를 반드시 해제합니다. 메모리 누수(Memory Leak)를 방지하는 중요한 역할을 합니다. 안전한 코드를 위해, 메모리 해제 후 포인터를 `nullptr`로 초기화합니다.

멤버 함수 (public)
외부에서 인벤토리 객체를 조작하기 위해 사용하는 기능들입니다.
`void AddItem(const T& item)`
새로운 아이템을 인벤토리에 추가합니다.
`size_`가 `capacity_`보다 작을 경우에만 아이템을 추가하고, `size_`를 1 증가시킵니다.
인벤토리가 꽉 찼다면 "인벤토리가 꽉 찼습니다!" 메시지를 출력하고 아무 동작도 하지 않습니다.
`void RemoveLastItem()`
인벤토리의 가장 마지막에 추가된 아이템을 제거합니다.
실제로 메모리를 지우는 것이 아니라, 아이템의 개수를 나타내는 `size_`를 1 감소시켜 마지막 아이템에 접근할 수 없도록 만듭니다.
인벤토리가 비어있다면 "인벤토리가 비어있습니다." 메시지를 출력합니다.
`int GetSize() const`
현재 인벤토리에 저장된 아이템의 개수(`size_`)를 반환합니다.
`int GetCapacity() const`
인벤토리의 최대 저장 용량(`capacity_`)을 반환합니다.
`void PrintAllItems() const`
인벤토리에 있는 모든 아이템의 정보를 화면에 출력합니다.
`for` 반복문을 이용해 `0`번 인덱스부터 `size_ 1`번 인덱스까지 순회하며, 각 아이템 객체의 `PrintInfo()` 멤버 함수를 호출합니다.

만약 인벤토리가 비어있다면 "(비어있음)" 메시지를 출력합니다.


인벤토리

#pragma once
#include <iostream>
#include "Item.h"

using namespace std;

template <typename T>
class Inventory
{ // 멤버 변수
	T* pItems; // 아이템 담을 가방(배열)
	int capacity; // 가방 최대 공간
	int size; // 현재 아이템 개수

public:
	Inventory(int cap = 10) : capacity(cap <= 0 ? 1 : cap), size(0) // 생성자 호출시 cap 0 이하일 때 1로 보정, size 0
	{
		pItems = new T[capacity]; // 힙 메모리에 메모리 공간 할당
	}
	~Inventory() // 소멸자 호출시 메모리 해제 후 포인터 초기화
	{
		delete[] pItems;
		pItems = nullptr;
	}

	void AddItem(const T& item) // 아이템을 원소로 추가하고 size + 1
	{
		if (size < capacity)
		{
			pItems[size] = item;
			++size;
		}
		else
		{
			cout << "인벤토리가 꽉 찼습니다!" << endl;
		}
	}

	void RemoveLastItem() // size - 1로 메모리에는 원소가 있지만 접근하지 못하도록함
	{
		if (size > 0)
		{
			--size;
		}
		else
		{
			cout << "인벤토리가 비어있습니다." << endl;
		}
	}

	int GetSize() const
	{
		return size;
	}

	int GetCapacity() const
	{
		return capacity;
	}

	void PrintAllItems() const
	{
		if (size <= 0)
		{
			cout << "(비어있음)" << endl;
		}
		else
		{
			for (size_t i = 0; i < size; ++i)
			{
				Item* item = dynamic_cast<Item*>(&pItems[i]); // pItems의 원소가 Item인지 확인하고 아니면 nullptr이 들어감.

				if (item != nullptr)
				{
					item->PrintInfo();
				}
			}
		}
	}
};

다른 부분은 과제에 나온대로 작성만 하면 돼서 막힘 없이 쭉 풀렸지만 PrintAllItems 함수에서 다이나믹 캐스트라는 새로운 개념을 알게 되어 사용을 했다. 아직 확실하게 이해는 안됐지만 다이나믹 캐스트로 런타임 중에 pItems의 원소가 Item 객체인지 확인을 하고 맞으면 item에 Item객체의 포인터를 아니면 nullptr을 반환하는 것 같다.

아이템

#pragma once
#include <string>
#include <iostream>

class Item
{
	std::string name;
	int price;

public:
	Item() {}
	Item(std::string name, int price);
	void PrintInfo() const;
	
};
#include "Item.h"

Item::Item(std::string, int price) : name(name), price(price) {}

void Item::PrintInfo() const
{
	std::cout << "[이름: " << name << ", 가격: " << price << "G]" << std::endl;
}

메인

#include "Inventory.h"
#include <iostream>

using namespace std;

int main()
{
	Inventory<Item> inv(5);
	Item sword("검", 50);
	Item shield("방패", 40);
	Item bow("활", 60);
	Item arrow("화살 50개", 20);

	inv.AddItem(sword); // 아이템 4개 추가
	inv.AddItem(shield);
	inv.AddItem(bow);
	inv.AddItem(arrow);

	cout << "====== 아이템 목록 ======" << endl;
	inv.PrintAllItems();
	cout << "현재 아이템 개수: " << inv.GetSize() << endl;

	cout << "====== 아이템 제거 후 ======" << endl;
	inv.RemoveLastItem(); // 아이템 2개 제거
	inv.RemoveLastItem();
	inv.PrintAllItems();
	cout << "현재 아이템 개수: " << inv.GetSize() << endl;

}


도전 기능

복사 생성자 및 대입(Assign) 함수 구현
인벤토리가 복사/대입될 때, 아이템 데이터가 그대로 복사되는 깊은 복사(Deep Copy)를 구현합니다.
`Inventory(const Inventory<T>& other);` (복사 생성자)
객체가 생성+초기화될 때 호출

// 복사 생성자
Inventory(const Inventory<T>& other) {
    capacity_ = other.capacity_;
    size_ = other.size_;
    pItems_ = new T[capacity_];
    for (int i = 0; i < size_; ++i) {
        pItems_[i] = other.pItems_[i];
    }
    cout << "인벤토리 복사 완료" << endl;
}


`void Assign(const Inventory<T>& other);` (대입 역할을 하는 함수)
이미 존재하는 객체에 값을 덮어쓸 때 호출
`AddItem` 기능 확장
인벤토리가 꽉 찬 상태에서 `AddItem`이 호출되면, 기존 `capacity`의 2배 크기로 인벤토리를 확장하고 새로운 아이템을 추가합니다. (기존 아이템들은 모두 유지되어야 합니다.)
`Resize` 멤버 함수 추가
`void Resize(int newCapacity)`: 인벤토리의 `capacity`를 `newCapacity`로 변경합니다.
`SortItems` 멤버 함수 추가
`void SortItems()`: 인벤토리의 아이템들을 가격 오름차순으로 정렬합니다. 
이 기능은 C++ 표준 라이브러리의 `std::sort` 알고리즘을 활용하며, 정렬 기준을 정의하기 위해 클래스 외부에 별도로 작성된 비교 함수(`compareItemsByPrice`)를 `std::sort`에 전달합니다.


1. `Inventory(const Inventory<T>& other);` (복사 생성자)

`void Assign(const Inventory<T>& other);` (대입 역할을 하는 함수)

// 도전 기능 - 복사 생성자,
Inventory(const Inventory<T>& inv) // 호출시 다른 Inventory 객체를 복사함.
{
	capacity = inv.capacity;
	size = inv.size;
	pItems = new T[capacity]; // 같은 capacity와 size를 가진 새로운 배열 생성
	for (int i = 0; i < size; ++i)
	{
		pItems[i] = inv.pItems[i]; // 원소 복사
	}
	cout << "인벤토리 복사 완료" << endl;
}

void Assign(const Inventory<T>& inv) // 대입 연산자
{
	delete[] pItems; // 원래 있던 배열 메모리 해제

	capacity = inv.capacity;
	size = inv.size;
	pItems = new T[capacity]; // 같은 capacity와 size를 가진 새로운 배열 생성
	for (int i = 0; i < size; ++i)
	{
		pItems[i] = inv.pItems[i]; // 원소 대입
	}
}

2. `AddItem` 기능 확장
인벤토리가 꽉 찬 상태에서 `AddItem`이 호출되면, 기존 `capacity`의 2배 크기로 인벤토리를 확장하고 새로운 아이템을 추가합니다. (기존 아이템들은 모두 유지되어야 합니다.)

void AddItem(const T& item) // 아이템을 원소로 추가하고 size + 1
{
	if (size < capacity)
	{
		pItems[size] = item;
		++size;
	}
	else
	{
		cout << "인벤토리가 꽉 차서 2배로 확장됐습니다." << endl;

		// 도전기능 - AddItem 기능 확장
		capacity *= 2;
		T* tempInv = new T[capacity]; // 공간 2배인 임시 배열 생성

		for (int i = 0; i < size; ++i)
		{
			tempInv[i] = pItems[i]; // 임시 배열에 원소 똑같이 넣기
		}
		delete[] pItems; // 기존배열 메모리 해제

		tempInv[size] = item; // 새로운 아이템 추가

		pItems = tempInv; // pItems에 임시 배열 주소 넣기

		++size;
	}
}

3. `Resize` 멤버 함수 추가
`void Resize(int newCapacity)`: 인벤토리의 `capacity`를 `newCapacity`로 변경합니다.

void Resize(int newcap)
{
	capacity = newcap;

	if (capacity <= 0) // 0 이하일 때 1로 보정
	{
		capacity = 1;
	}

	T* tempInv = new T[capacity]; // newcap 공간을 가진 임시 배열 생성

	if (size > capacity) // 임시 공간이 더 작으면
	{
		size = capacity;

		for (int i = 0; i < capacity; ++i)
		{
			tempInv[i] = pItems[i];
		}
	}
	else
	{
		for (int i = 0; i < size; ++i)
		{
			tempInv[i] = pItems[i];
		}
	}
	delete[] pItems;

	pItems = tempInv;
}

4. `SortItems` 멤버 함수 추가
`void SortItems()`: 인벤토리의 아이템들을 가격 오름차순으로 정렬합니다. 
이 기능은 C++ 표준 라이브러리의 `std::sort` 알고리즘을 활용하며, 정렬 기준을 정의하기 위해 클래스 외부에 별도로 작성된 비교 함수(`compareItemsByPrice`)를 `std::sort`에 전달합니다.

void SortItems()
{
	sort(pItems, pItems + size, compareItemsByPrice);
}

static bool compareItemsByPrice(const Item& a, const Item& b)
{
	return a.GetPrice() < b.GetPrice();
}

함수안에 멤버 변수를 사용하지 않으면 앞에 static을 붙여서 sort 같은 데에 넘길 때 일반 함수 포인터처럼 쓸 수 있다. 사실 무슨말인진 잘 모르겠다. 함수에 멤버 변수를 사용하지 않을 때 static을 붙이는게 대부분 적절하다고 한다.

메인

#include "Inventory.h"
#include <iostream>

using namespace std;

int main()
{
	Inventory<Item> inv(5);
	Item sword("검", 50);
	Item shield("방패", 40);
	Item bow("활", 60);
	Item arrow("화살 50개", 20);

	inv.AddItem(sword); // 아이템 6개 추가 -> capacity보다 많이 넣음
	inv.AddItem(shield);
	inv.AddItem(bow);
	inv.AddItem(arrow);
	inv.AddItem(sword);
	inv.AddItem(sword);

	cout << "====== 아이템 목록 ======" << endl;
	inv.PrintAllItems();
	cout << "현재 아이템 개수: " << inv.GetSize() << endl;
	cout << "현재 인벤토리 크기: " << inv.GetCapacity() << endl;

	cout << "====== 아이템 제거 후 ======" << endl;
	inv.RemoveLastItem(); // 아이템 2개 제거
	inv.RemoveLastItem();
	inv.PrintAllItems();
	cout << "현재 아이템 개수: " << inv.GetSize() << endl;
	cout << "현재 인벤토리 크기: " << inv.GetCapacity() << endl;
	system("pause");
	system("cls");

	cout << "====== inv2에 inv 복사 ======" << endl;
	Inventory<Item> inv2(inv);
	inv2.PrintAllItems();
	cout << "현재 아이템 개수: " << inv2.GetSize() << endl;
	cout << "현재 인벤토리 크기: " << inv2.GetCapacity() << endl;

	cout << "====== inv2 인벤토리 크기 조정 ======" << endl;
	inv2.Resize(3);
	inv2.PrintAllItems();
	cout << "현재 아이템 개수: " << inv2.GetSize() << endl;
	cout << "현재 인벤토리 크기: " << inv2.GetCapacity() << endl;

	cout << "====== inv2 금액순으로 정렬 ======" << endl;
	inv2.SortItems();
	inv2.PrintAllItems();
	cout << "현재 아이템 개수: " << inv2.GetSize() << endl;
	cout << "현재 인벤토리 크기: " << inv2.GetCapacity() << endl;
}

 

복습을 하면서 처음 과제를 풀 때 보다는 그래도 더 이해가 되는 상태로 풀게 되어 조금은 성장한 것 같지만 아직 모르는 개념이 너무 많아서 새로운 개념들이 등장할 때 이것을 이해하려고 시간이 너무 많이 들어서 힘들다. 이번에 새로 등장한 다이나믹 캐스트, static 같은 것들을 더 공부를 해서 이해를 완벽하게 해야될 것 같다.