Unreal 개발 본 캠프 15일차

HappyCircle·2025년 12월 17일

Unreal 개발

목록 보기
32/163

오늘의 학습 진행 내용

[3번 과제] 인벤토리 시스템 구현

템플릿을 활용하여 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;
      }
    • 만약 인벤토리가 비어있다면 "(비어있음)" 메시지를 출력합니다.

구현을 한 뒤에 클래스의 구조는 아래와 같습니다.

도전 기능 가이드
필수 기능을 모두 완료했다면, 실제 게임의 인벤토리처럼 더욱 유연한 기능을 추가해 봅시다!

  • 복사 생성자 및 대입(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이 호출되면, 기존 capacity2배 크기로 인벤토리를 확장하고 새로운 아이템을 추가합니다. (기존 아이템들은 모두 유지되어야 합니다.)
  • Resize 멤버 함수 추가:
    • void Resize(int newCapacity): 인벤토리의 capacitynewCapacity로 변경합니다.
  • SortItems 멤버 함수 추가:
    • void SortItems(): 인벤토리의 아이템들을 가격 오름차순으로 정렬합니다.
      이 기능은 C++ 표준 라이브러리의 std::sort 알고리즘을 활용하며, 정렬 기준을 정의하기 위해 클래스 외부에 별도로 작성된 비교 함수(compareItemsByPrice)를 std::sort에 전달합니다.
      //두 Item 객체를 받아 가격을 기준으로 어떤 것이 먼저 와야 하는지 판단
      bool compareItemsByPrice(const Item& a, const Item& b) {
          return a.GetPrice() < b.GetPrice();
      }

필수 기능과 도전 기능 구현된 구조는 다음과 같습니다.

실습 코드(구현 중)

Inventory.h

#pragma once
#include <iostream>
#include <string>
#include <algorithm>
#include "compareItemByPrice.h"
using namespace std;
template <typename T>
class Inventory {
public:
	/*
	* 필수 기능
	생성자: `Inventory(int capacity = 10)`**
    - 인벤토리 객체가 생성될 때 호출됩니다.
    - 매개변수로 인벤토리의 용량(`capacity`)을 받으며,
	값을 전달하지 않으면 **기본값으로 10**이 설정됩니다.
	size_는 값 0으로 인벤토리 객체 생성 시 존재하는 아이템 없으므로 0으로 초기화
	pItems_ 인벤토리 동적 배열은 capcity 용량에 맞게 동적 할당 선언
	*/
	Inventory(int capacity = 10) : capacity_(capacity),size_(0) {
		//cacacity 인벤토리 용량은 논리적으로 0보다 커야되므로 최소값 1로 설정
		if (capacity <= 0) {
			capacity_ = 1;
		}
		pItems_ = new T[capacity_];
	};
	/*
	* 도전 기능
	복사 생성자
	객체가 생성+초기화될 때 호출, 
	새로운 Inventory 객체가 기존 Inventory 객체로 초기화될 때 호출됩니다.
	기존 객체의 capacity_, size_, pItems_ 내용을 새로운 객체에 깊은 복사(Deep Copy)합니다.
	예시:
	Inventory<int> A;
	Inventory<int> B = A;  // 복사 생성자 호출
	*/
	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;
	}
	/*
	* 필수 기능
	* 소멸자
	 인벤토리 객체가 소멸될 때 (예: `main` 함수 종료 시) 자동으로 호출됩니다.
     `delete[] pItems_`를 실행하여 생성자에서 할당했던 메모리를 **반드시 해제**합니다. 메모리 누수(Memory Leak)를 방지하는 중요한 역할을 합니다.
     안전한 코드를 위해, 메모리 해제 후 포인터를 `nullptr`로 초기화합니다.
	*/
	~Inventory() {
			delete[] pItems_;   // 동적 할당 해제
			pItems_ = nullptr;  // 안전을 위해 nullptr 초기화
			cout << "인벤토리 소멸" << endl;
	};
	/*
	`void AddItem(const T& item)`**
	* 필수 기능
    새로운 아이템을 인벤토리에 추가합니다.
    `size_`가 `capacity_`보다 작을 경우에만 아이템을 추가하고,
	`size_`를 1 증가시킵니다.
    인벤토리가 꽉 찼다면 "인벤토리가 꽉 찼습니다!" 메시지를 출력하고 
	아무 동작도 하지 않습니다.(도전 기능에서 인벤토리 용량 늘리기로 변경)
	* 도전 기능
	인벤토리가 꽉 찬 상태에서 AddItem이 호출되면,
	기존 capacity의 2배 크기로 인벤토리를 확장하고 새로운 아이템을 추가합니다. 
	(기존 아이템들은 모두 유지되어야 합니다.)
	*/
	void AddItem(const T& item) {
		if (size_ >= capacity_) {
			cout << "인벤토리가 꽉 찼습니다!" << endl;
			this->Resize(capacity_ * 2);
		}
		pItems_[size_++] = item;
	}
	/*
	* 필수 기능
	 인벤토리의 가장 마지막에 추가된 아이템을 제거합니다.
    실제로 메모리를 지우는 것이 아니라, 
	아이템의 개수를 나타내는 `size_`를 1 감소시켜 
	마지막 아이템에 접근할 수 없도록 만듭니다.
    인벤토리가 비어있다면 "인벤토리가 비어있습니다." 메시지를 출력합니다.
	*/
	void RemoveLastItem() {
		if (size_ > 0) {
			size_--;
		}
		else {
			cout << "인벤토리가 비어있습니다."<<endl;
		}
	}
	/*
	* 필수 기능
	현재 인벤토리에 저장된 아이템의 개수(size_)를 반환합니다.
	*/
	int GetSize() const { //const 처리로 GetSize() 내부에서는 객체의 멤버를 변경 불가능.
		return size_;
	}

	/*
	* 필수 기능
	인벤토리의 최대 저장 용량(`capacity_`)을 반환합니다.
	*/
	int GetCapacity() const {  //const 처리로 GetCapacity() 내부에서는 객체의 멤버를 변경 불가능.
		return capacity_;
	}
	/*
	필수 기능
	인벤토리에 있는 모든 아이템의 정보를 화면에 출력합니다.
	`for` 반복문을 이용해 `0`번 인덱스부터 `size_ - 1`번 인덱스까지 순회하며,
	각 아이템 객체의 `PrintInfo()` 멤버 함수를 호출함
	만약 인벤토리가 비어있다면 "(비어있음)" 메시지를 출력
	*/
	void printAllItems() const {  //const 처리로 printAllItems() 내부에서는 객체의 멤버를 변경 불가능.
		if (size_ == 0) {
			cout << "인벤토리가 비어있습니다." << endl;
		}
		else {
			for (int i = 0; i < size_; i++) {
				pItems_[i].PrintInfo(); //pItems_[i] 객체에 선언된 PrintInfo() 멤버 함수 호출
			}
		}
	}
	/*
	* 도전 기능
	대입 역할을 하는 함수
     - 이미 존재하는 객체에 값을 덮어쓸 때 호출
	Inventory<int> A , Inventory<Int> B 이런식으로 선언되어 있을 때
	예시:
	Inventory<int> A, B;
	A.Assign(B); // A에 B 내용 복사
	*/
	*/
	void Assign(const Inventory<T>& other) {
		if (this == &other) return;  // 자기 자신 대입 방지
		
		delete[] pItems_; //기존 메모리 해제

		size_ = other.size_;
		capacity_ = other.capacity_;
		pItems_ = new T[capacity_]; //새로운 배열 할당(대입시킬 배열)

		//요소 복사
		for (int i = 0; i < size_; ++i) {
			pItems_[i] = other.pItems_[i];
		}
	};
	/*
	* 도전 기능
	인벤토리의 capacity를 newCapacity로 변경
	변경 시 기존 배열에 존재하던 아이템들은 그대로 유지한 상태로
	배열의 용량만 늘리기 위해서 신규 배열 newCapcity 크기로 동적 할당
	후 기존 요소 복사해서 처리
	*/
	void Resize(int newCapacity) {
		if (newCapacity <= 0) return;

		T* newItems = new T[newCapacity];
		int elementsToCopy = (size_ < newCapacity) ? size_ : newCapacity;
		for (int i = 0; i < elementsToCopy; ++i) {
			newItems[i] = pItems_[i];
		}
		delete[] pItems_;
		pItems_ = newItems;
		capacity_ = newCapacity;
		size_ = elementsToCopy;
	}
	/*
	인벤토리의 아이템들을 가격 오름차순으로 정렬합니다. 
	이 기능은 C++ 표준 라이브러리의 std::sort 알고리즘을 활용하며, 
	정렬 기준을 정의하기 위해 클래스 외부에 별도로 작성된 
	비교 함수(compareItemsByPrice)를 std::sort에 전달합니다.
	STL algorithm의 sort 가져와서 pItems 동적 배열 포인터로 
	정렬 시작,끝 범위 지종후
	외부 정의된 비교함수 적용하는 형태로 구현
	*/
	void SortItems() {
		sort(pItems_,pItems_+size_, compareItemsByPrice);
	}
private:
	T* pItems_;
	int capacity_;
	int size_;

};

C++ 복습 점프업 세션

복습 내용 중 개념 헷갈리거나 정리하고 싶었던 내용 정리

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Person {
public:
    Person() { cout << "Person\n"; }
};

class Student : public Person {
public:
    Student() { cout << "Student\n"; }
};

    //Smart Pointer 부분이랑 메모리 컨트롤 부분 약한 부분 개념 공부 복습하기
    //weak_ptr 사용시 실제 소유권이 필요한 곳만 shared_ptr, 나머지는 weak_ptr로 선언
    //weak_ptr은 항상 shared_ptr.lock() 객체 살아있는지(소유권 갖고 있는지) 체크
    //B의 소유권은 A가 갖고 있는 형태 

    struct B;

    struct A {
        shared_ptr<B> b;
        ~A() { cout << "~A" << endl; }
    };

    struct B {
        weak_ptr<A> a;    // weak_ptr로 변경 해서 순환 참조 해결
        ~B() { cout << "~B" << endl; }

        void CheckA() {
            if (auto sp = a.lock()) {
                cout << "A is alive" << endl;
            } else {
                cout << "A is expired" << endl;
            }
        }
};

//템플릿 함수 호출 시 타입 쓰지 않아도 되는 이유, 함수에 놓은 값을 보고 타입을 맞춰 주기 때문
template <typename T>
T Add(T a, T b)
{
    return a + b;
}
//템플릿 함수는 하나의 타입만 받는게 아니라 여러 개의 타입을 받아서 사용 가능
template <typename T1, typename T2>
auto Add(T1 a, T2 b)
{
    return a + b;
}
// Add(3, 3.5) 이런 형태로도 사용이 가능

//템플릿 함수 Call By Reference를 통해 원본 변수의 값 변경

template<typename T>
void Swap(T& a, T& b)
{
    T temp = a;
    a = b;
    b = temp;
}

int main(){
    int* p1 = new int(10);
    int* p2 = p1;
    
    delete p1;
    p1 = nullptr;
    p2 = nullptr; //없는 주소값 가리키는 포인터가 되버린 p2를 nullptr로 재정의

    //Person -> Student (부모 클래스 먼저 호출되고 그 후에 자식 Class 호출)

    //unique_ptr 선언
    unique_ptr<int> p1_u = make_unique<int>(10);

    unique_ptr<int> p2_u;

    p2_u = move(p1_u);

    cout << "p1 is " << (p1_u ? "not null" : "null") << endl;
    cout << "p2 value = " << *p2_u << "\n";

    //shared_ptr은 참조 받을때마다 +1이되고 rest을 사용하면 0으로 초기화, {} 끝날 때마다도 -1된다.
    std::shared_ptr<int> sp1 = std::make_shared<int>(10);
    std::shared_ptr<int> sp2 = sp1; // 참조 카운트 +1
    {
    std::shared_ptr<int> sp3 = sp1; // 참조 카운트 +1
    } //여기서 참조 카운트 -1
    //참조 대상이 살아있는지 확인하고 살아있으면 참조카운트+1하고 shared_ptr 생성,
    //삭제된 상태면 nullptr shared_ptr이 반환됨

    std::weak_ptr<int> wp = sp1;
    if (auto sp = wp.lock()) {
    // 여기 들어왔다는 건 객체가 살아있다는 뜻
    // sp->doSomething(); // 안전
    } else {
        // 이미 삭제됨
    }

    auto a = make_shared<A>();
    auto b = make_shared<B>();

    a->b = b;
    b->a = a;

    b->CheckA();
    
    cout << "End of main" << endl;

/*    //operator로 연산자 함수로 재정의해서 overroading 하는 케이스로 사용 가능
    Vec2 operator+(const Vec2& other) const {
        //현재 객체(*this)와 x,y와
        //othher 객체의 x,y를 각각 더한
        //새로운 Vec2 객체를 반환
        return Vec2(x+other.x,y+other.y);
    }
    //정의 되있지 않은 operator의 경우 신규 생성도 가능
    ostream& operator<<(ostream& os, const Point& p){
        //출력 스트림(os)에 Point 정보 출력
        os<<"("<<p.x<<","<<p.y<<")";

        //스트림을 다시 반환해야
        //cout<<a<<b<<c와 같은 체이닝이 가능
        return os;
    } 
*/

    int x = 10;
    int y = 20;

    Swap(x, y);

    cout << "x: " << x << ", y: " << y << endl;

     //auto는 “이미 타입이 결정돼 있고, 반복해서 쓰기 싫을 때” 쓰는 도구
    //auto는 값 복사 기준이어서 참조와 const는 버리고 처리함
    //초기값이 없는 경우 불가능
    //decltype은 표현식의 타입을 그대로 가져와라
    //decltype은 참조/const를 절대 버리지 않음
;
    decltype(x)   a_d;  // int
    decltype((x)) b_d = x; // int&  ← 괄호 때문에!


}
profile
개발합시다!

0개의 댓글