이 Step에서 다루는 것

  • 인벤토리를 Item* 배열로 설계할 때의 핵심 계약(소유권/삭제 책임)
  • 전방 선언(Forward Declaration)으로 헤더 의존성을 줄이는 방법
  • 순환 참조가 발생하는 이유와 실전에서 끊는 패턴

학습 목표

  • AddItem/RemoveItem API에서 “누가 delete 책임인지”를 명확히 설계할 수 있다.
  • class Item;만으로 가능한 것/불가능한 것을 구분할 수 있다.
  • 순환 include를 전방 선언 + cpp include로 분리해 해결할 수 있다.

인벤토리 구조: “포인터 배열”은 곧 소유권 설계 문제

Item* _items[MAX_SLOT]는 “아이템 객체를 직접 저장”하는 게 아니라,
힙에 있는 아이템을 가리키는 주소만 저장합니다.

Inventory::_items
  [0] -> Weapon 객체(힙)
  [1] -> Armor  객체(힙)
  [2] -> nullptr
  ...

따라서 핵심은 하나입니다.

  • 삭제 책임을 누구에게 둘 것인가?

권장 계약 예시(학습용 raw pointer 기준):

  • bool AddItem(Item* item)
    • 성공: 인벤토리가 소유권을 가져감(인벤토리가 나중에 delete)
    • 실패: 소유권은 호출자에게 남음(호출자가 delete)
  • Item* RemoveItem(int slot)
    • 슬롯에서 꺼내서 반환(소유권을 호출자에게 넘김)
    • 호출자가 책임지고 delete
class Inventory {
public:
    static constexpr int MAX_SLOT = 20;

    bool AddItem(Item* item);     // 성공 시 소유권 획득
    Item* RemoveItem(int slot);   // 소유권 반환(호출자에게 이동)
    Item* GetItemAtSlot(int slot) const;

private:
    Item* _items[MAX_SLOT] = {};
};

실전에서는 std::unique_ptr<Item>로 소유권을 타입에 담는 방식이 더 안전합니다.


전방 선언(Forward Declaration): 헤더를 가볍게

헤더에서 Item을 “포인터/참조”로만 다루면 정의가 없어도 됩니다.

// Inventory.h
#pragma once
class Item; // 전방 선언

class Inventory {
private:
    Item* _items[20] = {};
};

그리고 실제 구현이 필요한 cpp에서만 include 합니다.

// Inventory.cpp
#include "Inventory.h"
#include "Item.h" // 여기서 실제 정의 필요

불완전 타입에서 가능한 것 / 불가능한 것

  • 가능: Item*, Item&, 함수 선언의 인자/반환 타입
  • 불가능: Item 객체를 값으로 멤버 보관, sizeof(Item), 멤버 접근(item->PrintInfo() 등)
    → 이런 작업은 정의가 필요하므로 cpp에서 include 후 수행

순환 참조 끊기: include를 헤더가 아니라 구현으로 내리기

문제 패턴:

  • Item.h#include "Inventory.h"
  • 동시에 Inventory.h#include "Item.h"
    -> 서로가 서로를 요구하는 구조가 되어 컴파일이 꼬입니다.

해결 패턴:

헤더(.h):
  필요한 타입이 포인터/참조면 -> 전방 선언만

소스(.cpp):
  실제 구현이 필요한 곳에서만 include

이렇게 하면:

  • 순환 참조를 끊고
  • 빌드 시간/의존성도 줄어듭니다.

체크 질문 (스스로 답해보기)

  • AddItem이 실패했을 때 누가 delete해야 하는가?
  • 전방 선언만 있는 상태에서 Item 객체를 값으로 멤버에 둘 수 없는 이유는?
  • 순환 include를 끊을 때 “어떤 include를 cpp로 내릴지” 어떻게 판단할까?

profile
李家네_공부방

0개의 댓글