포스트

C++ 상태창·전투 시스템·템플릿 인벤토리

깊은 복사를 직접 짜다 만난 double free

배열·포인터로 상태창을 만들고, 상속·다형성으로 전투를 얹고, 템플릿과 직접 짠 동적 배열로 인벤토리를 만드는 3단 구성. 한글 깨짐·LNK2019 같은 빌드 트러블슈팅까지.

C++ 상태창·전투 시스템·템플릿 인벤토리

C++ 과제 세 개는 따로 떨어진 문제가 아니라 한 줄기로 이어져 있었다. 배열·포인터 기초로 상태창을 만들고, 그 위에 상속·다형성으로 전직·전투를 얹고, 마지막에 템플릿과 직접 짠 동적 배열로 인벤토리를 만드는 구성이다. 이 글에서는 그 3단을 순서대로 이야기하려 한다 — 각 단계의 구현이 앞이고, 깊은 복사를 직접 짜다 만난 double free와 한글 깨짐·LNK2019 같은 빌드 문제는 뒤에 트러블슈팅으로 모았다.

과제 1 — 상태창 구현 (C++ 기초)

핵심 개념

개념내용
배열int stat[4]stat[0]=HP, stat[1]=MP, stat[2]=공격력, stat[3]=방어력
포인터 매개변수void setPotion(int count, int* p_HPPotion, int* p_MPPotion)
while + switch0번 입력 전까지 메뉴 반복 출력
입력 유효성 검사HP·MP > 50, 공격력·방어력 > 0 강제

핵심 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 포인터로 값 직접 수정
void setPotion(int count, int* p_HPPotion, int* p_MPPotion) {
    *p_HPPotion = count;
    *p_MPPotion = count;
}

// 무한루프 메뉴
while (1) {
    cin >> choice;
    if (choice == 0) break;
    switch (choice) {
        case 1:
            if (HPPotion <= 0) { cout << "포션이 부족합니다." << endl; continue; }
            stat[0] += 20;
            HPPotion--;
            break;
    }
}

배운 점

* 역참조로 포인터가 가리키는 값을 함수 안에서 직접 바꾸는 방식이 손에 익었다. 상수는 const int SIZE = 4;처럼 const로 선언했고, 포션이 0개일 때는 continue로 루프를 다시 시작하는 흐름 제어를 썼다.


과제 2 — 전직 시스템 & 전투 시스템 (OOP)

핵심 개념

개념설명
추상 클래스virtual void attack() = 0 → 순수 가상 함수로 자식 구현 강제
상속class Warrior : public Player
다형성Player* player = new Warrior(nick) → 부모 포인터로 자식 동작
가상 소멸자virtual ~Player() → 자식 객체 안전 소멸
오버로딩attack() / attack(Monster*) 두 버전 공존

파일 구조

1
2
3
4
5
6
7
assignment-2/
├── player.h / player.cpp       # 추상 기반 클래스
├── warrior.h / warrior.cpp     # 전사 (HP 150, 검 공격)
├── magician.h / magician.cpp   # 마법사 (MP 150, 마법 화살)
├── thief.h / thief.cpp         # 도적 (5회 연속 공격)
├── archer.h / archer.cpp       # 궁수 (3회 연속 공격)
└── monster.h / monster.cpp     # 몬스터 (HP 10, 공격력 30)

핵심 코드

1
2
3
4
5
6
7
8
9
10
11
12
// 다형성 활용
Player* player = nullptr;
switch (job_choice) {
    case 1: player = new Warrior(nickname);  break;
    case 2: player = new Magician(nickname); break;
    case 3: player = new Thief(nickname);    break;
    case 4: player = new Archer(nickname);   break;
}
player->attack();           // 직업에 맞는 attack() 자동 호출
player->attack(monster);    // 오버로딩된 버전 호출

if (player != nullptr) delete player;  // 메모리 해제

배운 점

순수 가상 함수(= 0)를 선언하면 클래스가 추상 클래스가 되고, 자식이 반드시 구현하도록 강제된다는 걸 확인했다. 헤더끼리 서로를 참조하는 순환 참조는 전방 선언(Forward Declaration — class Monster;처럼 이름만 미리 알려 두는 것)으로 끊었다. 빌드에서는 LNK2019 링커 오류를 만났는데, cl.exe.cpp 파일을 하나만 넣으면 다른 파일의 심볼을 찾지 못해서 생기는 문제였다. 모든 .cpp를 함께 전달해야 한다.


과제 3 — 인벤토리 시스템 (템플릿 & 메모리 관리)

핵심 개념

개념설명
템플릿template <typename T> — 타입에 무관한 범용 클래스
동적 배열T* pItems_ = new T[capacity_]
깊은 복사복사 생성자에서 new T[]로 새 메모리 할당 후 데이터 복사
자동 확장(Resize)꽉 차면 capacity_ * 2 크기로 재할당
std::sortsort(pItems_, pItems_ + size_, compareItemsByPrice)

핵심 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
class Inventory {
public:
    Inventory(int capacity = 5);              // Default Argument
    Inventory(const Inventory<T>& other);     // 복사 생성자 (깊은 복사)
    ~Inventory() { delete[] pItems_; pItems_ = nullptr; }

    bool AddItem(const T& item);   // 꽉 차면 Resize() 자동 호출
    bool RemoveLastItem();         // size_-- 만으로 논리적 제거
    void SortItems();              // std::sort 활용
    Inventory<T>& Assign(const Inventory<T>& other);  // 대입 깊은 복사

private:
    void Resize(int newCapacity);
    T*  pItems_;
    int capacity_, size_;
};

Resize 내부 동작 흐름

1
2
3
4
5
1. new T[newCapacity]   →  새 메모리 할당
2. 기존 데이터 복사      →  for (i < size_) newItems[i] = pItems_[i]
3. delete[] pItems_     →  기존 메모리 해제
4. pItems_ = newItems   →  포인터 교체
5. capacity_ 갱신

배운 점

이번 과제에서 제일 와닿은 건 얕은 복사와 깊은 복사의 차이다. 기본 복사는 포인터 주소만 복사하기 때문에 두 객체가 같은 메모리를 가리키고, 소멸 시 같은 메모리를 두 번 해제하는 double free가 터진다. new로 새 공간을 만들어 데이터를 복사해야 안전하다. 생성자는 여러 개 만들 필요 없이 Default Argument(Inventory(int capacity = 5)) 하나로 통합했다.

그리고 이 Inventory<T>가 사실상 std::vector의 핵심 동작(push_back, resize, 깊은 복사)을 직접 만들어 본 것이라는 점, Unreal의 TArray도 같은 원리로 동작한다는 점이 연결됐다.


트러블슈팅 — 빌드와 인코딩

문제원인해결
한글 깨짐 (?꾩궗)파일이 EUC-KR 인코딩, VS가 BOM 없이 CP949로 읽음UTF-8 BOM 추가 + /utf-8 컴파일 플래그
LNK2019 링커 오류cl.exeMain.cpp만 전달모든 .cpp 파일을 함께 컴파일
솔루션 용량 과다.vs/, Debug/, .obj 등 빌드 산출물 추적됨.gitignore에 패턴 기반 규칙 추가, 캐시 삭제

언리얼 엔진과의 연결고리

  • 과제 1 → 게임 오브젝트 상태 관리의 기초 (HP바, 버프 시스템)
  • 과제 2AActor / APawn 상속 구조, UFUNCTION 오버라이딩
  • 과제 3TArray<T> 내부 원리 이해, 메모리 관리 역량

핵심 요약 — 깊은 복사를 직접 구현하며 double free가 왜 생기는지 체감했고, 동적 배열 + 자동 확장 구조가 곧 std::vectorTArray의 내부 원리라는 걸 연결해서 이해했다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.