과제 목표:
C++ 템플릿(Template)을 사용하여 다양한 타입의 아이템을 저장할 수 있는 인벤토리 클래스를 구현합니다.
동적 메모리 할당과 해제를 직접 관리하며, C++ STL 컨테이너 중 std::vector의 핵심 동작 원리를 이해합니다.
이를 통해 제네릭 프로그래밍의 필요성과 메모리 관리의 중요성을 학습합니다.
필수 기능
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() 멤버 함수를 호출합니다.
- 만약 인벤토리가 비어있다면 "(비어있음)" 메시지를 출력합니다.
// 정보를 출력하는 멤버 함수
void PrintInfo() const {
cout << "[이름: " << name_ << ", 가격: " << price_ << "G]" << endl;
}
구현을 한 뒤에 클래스의 구조는 아래와 같습니다.
헤더
#pragma once
#include <iostream>
template<typename T>
class Inventory
{
private:
T* pItems; // 아이템을 받을 동적 배열
int capacity; // 가방 최대 공간
int size; // 아이템 개수
public:
Inventory(int capacity = 10) : capacity((capacity <= 0) ? capacity = 1 : capacity), size(0)
{
pItems = new T[capacity]; // 동적 배열에 가방 공간만큼 크기 생성
}
void AddItem(const T& item) // 아이템 추가함수
{
if (size < capacity) // 현재 아이템 개수가 가방 최대 공간보다 작으면
{
pItems[size] = item; // 가방에 아이템 저장
++size; // 다음 아이템 받기위해서 size + 1
}
else // 현재 아이템 개수가 가방 최대 공간 이상이라면
{
std::cout << "인벤토리가 꽉 찼습니다!" << std::endl;
}
}
void RemoveLastItem() // 아이템 제거함수
{
if (size > 0) // 아이템이 있다면
{
--size; //
}
else // 아이템이 없다면
{
std::cout << "인벤토리가 비어있습니다." << std::endl;
}
}
int GetSize() const
{
return size;
}
int GetCapacity() const
{
return capacity;
}
void PrintAllItems() const
{
for (int i = 0; i < size; ++i)
{ // 아직 아이템 클래스는 구현하지 않아서 구현할 때 name, price를 멤버변수 선언해야됨.
std::cout << "[이름: " << pItems[i].name << ", 가격: " << pItems[i].price << "G]" << std::endl;
}
}
~Inventory()
{
delete[] pItems; // 소멸시 동적 메모리 삭제
pItems = nullptr; // 빈 포인터로 만들기
}
};
헤더에서는 선언만하고 소스에서 구현을 하려 했지만 템플릿 클래스에서는 헤더에서 구현까지 해야 한다고 해서 했음.
도전 기능
필수 기능을 모두 완료했다면, 실제 게임의 인벤토리처럼 더욱 유연한 기능을 추가해 봅시다!
- 복사 생성자 및 대입(Assign) 함수 구현:
- 인벤토리가 복사/대입될 때, 아이템 데이터가 그대로 복사되는 깊은 복사(Deep Copy)를 구현합니다.
-
Inventory(const Inventory<T>& other); (복사 생성자)
- 객체가 생성 + 초기화될 때 호출
- void Assign(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;
}
- AddItem 기능 확장:
- 인벤토리가 꽉 찬 상태에서 AddItem이 호출되면 기존 capacity의 2배 크기로 인벤토리를 확장하고 새로운 아이템을 추가합니다. (기존 아이템은 모두 유지되어야 한다.)
- Resize 멤버 함수 추가:
- void Resize(int newCapacity): 인벤토리의 capacity를 newCapacity로 변경합니다.
- SortItems 멤버 함수 추가:
- void SortItems(): 인벤토리의 아이템들을 가격 오름차순으로 정렬합니다.
- 이 기능은 C++ 표준 라이브러리의 std::sort 알고리즘을 활용하며, 정렬 기준을 정의하기 위해 클래스 외부에 별도로 작성된 비교 함수(compareItemsByPrice)를 std::sort에 전달합니다.
//두 Item 객체를 받아 가격을 기준으로 어떤 것이 먼저 와야 하는지 판단
bool compareItemsByPrice(const Item& a, const Item& b) {
return a.GetPrice() < b.GetPrice();
}
1. 복사 생성자 및 대입(Assign)함수 구현
// 복사 생성자
Inventory(const Inventory<T>& other) // other는 인벤토리 객체
{
capacity = other.capacity;
size = other.size;
pItems = new T[capacity];
for (int i = 0; i < size; ++i) // size만큼
{
pItems[i] = other.pItems[i]; // 복사
}
cout << "인벤토리 복사 완료" << endl;
}
// 대입 역할 함수
void Assign(const Inventory<T>& other)
{
delete[] pItems; // 객체가 원래 가지고있던 배열 삭제.
capacity = other.capacity; // 값을 가지고 올 객체에서 멤버변수 값을 대입함.
size = other.size;
pItems = new T[capacity]; // 배열 새로 생성.
for (int i = 0; i < size; ++i)
{
pItems[i] = other.pItems[i];
}
}
복사와 대입의 코드는 둘다 복사 하는게 똑같지만 호출 타이밍이 다름.
복사는 객체가 생성될 때 원소를 복사, 대입은 존재하던 객체에 배열을 삭제하고 새로 만든 후 복사.
2. AddItem 기능 확장
void AddItem(const T& item)
{
if (size < capacity) // 필수 기능과 동일하게 item을 pItems의 원소로 받음
{
pItems[size] = item;
++size;
}
else // size가 capacity 이상이면
{
capacity = capacity * 2; // capacity 공간 2배
T* newItems = new T[capacity]; // 새로운 배열 생성
for (int i = 0; i < size; ++i) // size만큼 새로운 배열에 원소 복사
{
newItems[i] = pItems[i];
}
newItems[size] = item; // 복사 후 새로 넣은 아이템까지 추가
++size; // 추가 후 size + 1
delete[] pItems; // 원래 있던 배열 포인터 삭제.
pItems = newItems; // 배열 포인터에 newItems의 주소 대입.
}
}
3. Resize 멤버 함수 추가
void Resize(int newCapacity)
{
if (newCapacity <= 0) // 새로운 공간이 0이하면 1로 보정
{
newCapacity = 1;
}
T* newItems = new T[newCapacity]; // 새로운 배열을 newCapacity 크기로 생성
if (size > newCapacity) // 재할당된 공간이 기존 아이템 개수보다 작으면
{
size = newCapacity; // size를 재할당된 공간으로 맞춰서 그 뒤의 아이템 원소는 삭제
}
for (int i = 0; i < size ; ++i) // size 만큼 새로운 배열로 원소 복사
{
newItems[i] = pItems[i];
}
delete[] pItems; // 복사 후 원래 있던 배열 주소 메모리에서 삭제
pItems = newItems; // 새로운 배열 주소 대입
capacity = newCapacity;
}
4.SortItems 멤버 함수 추가
bool compareItemsByPrice(const Item& a, const Item& b)
{
return a.GetPrice() < b.GetPrice(); // a가 앞에 오는 원소, 작은거에서 큰순으로 -> 오름차순
}
void SortItems()
{
std::sort(pItems, pItems + size, compareItemsByPrice); // 배열 시작주소부터 마지막 원소의 다음 주소까지
}
이번 과제는 강의에서는 배웠지만 직접 작성해보려니 이해가 안되는게 너무 많아서 이해하면서 하느라 10시간이 넘게 걸렸다.
복사 생성자에서 인수로 받는 (const Inventory<T>& other)에서 other에 들어오는 게 T의 타입(여기서는 Item 클래스인줄 알았음)인줄 알았는데 Inventory 객체였다.
T는 단지 Inventory의 멤버변수 T* pItems 배열에 들어가는 원소의 타입이란 것을 깨닫기 전까지 오랜 시간이 걸려서 이해가 안되어 꺾일뻔 했지만 이해를 하고 나서 코드를 다시 읽어보며 이해가 돼서 기분이 좋았다.
템플릿을 처음 실습에서 처음 사용해봐서 이해하기 어려운게 많았던 것 같다.
delete pItems를 하면 pItems라는 배열이 아예 삭제 되는 걸로 알고 있었는데 pItems는 타입T를 원소로 가진 배열을 가리키는 포인터로 배열을 할당한 메모리만 삭제하고 새로운 배열을 new T[ ]로 선언하여 다시 pItems에 대입하여 사용할 수 있다는 것도 깨달았다.
그래도 이렇게 직접 동적 배열을 사용하여 벡터의 동작 원리를 깨달아 보니 벡터가 어떤 방식으로 동작하는지 알게 된 것 같다.
아직 이해가 안되는 부분이 AddItem 함수를 호출해서 pItems[0] = 검(Item 클래스)를 넣고 size가 +1 된 상태에서 RemoveLastItem 함수로 size만 -1 하면 pItems[0]에는 검 이 아직 남아있는 상태지만 이 값에 대해 접근을 하지 못한다라고 하는데 접근을 하지 못한다는게 뭔 소리인지 잘 이해가 안된다.
'숙제' 카테고리의 다른 글
C++ 4번 과제(연금술 공방 관리 시스템 구현) (1) | 2025.08.26 |
---|---|
C++ 3번 과제 테스트(인벤토리 시스템 구현) (5) | 2025.08.26 |
C++ 숙제 (영화 데이터를 관리하는 프로그램) (0) | 2025.08.21 |
C++ 숙제(SOLID원칙을 적용한 Student 클래스 구현) (0) | 2025.08.21 |
C++ 숙제(반복자를 활용하여 각 컨테이너를 순회하기) (0) | 2025.08.20 |