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 + switch | 0번 입력 전까지 메뉴 반복 출력 |
| 입력 유효성 검사 | 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::sort | sort(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.exe에 Main.cpp만 전달 | 모든 .cpp 파일을 함께 컴파일 |
| 솔루션 용량 과다 | .vs/, Debug/, .obj 등 빌드 산출물 추적됨 | .gitignore에 패턴 기반 규칙 추가, 캐시 삭제 |
언리얼 엔진과의 연결고리
- 과제 1 → 게임 오브젝트 상태 관리의 기초 (HP바, 버프 시스템)
- 과제 2 →
AActor / APawn 상속 구조, UFUNCTION 오버라이딩 - 과제 3 →
TArray<T> 내부 원리 이해, 메모리 관리 역량
핵심 요약 — 깊은 복사를 직접 구현하며 double free가 왜 생기는지 체감했고, 동적 배열 + 자동 확장 구조가 곧 std::vector와 TArray의 내부 원리라는 걸 연결해서 이해했다.