이 Step에서 다루는 것

  • 상속이 필요한 이유: 공통 기능/데이터 재사용 + 타입을 하나로 묶어 다루기
  • 상속이 “무조건 좋은 것”이 아니라, is-a일 때만 맞는 도구라는 기준
  • 상속에서 자주 터지는 함정: 객체 slicing(잘림), 부모 포인터로 접근 한계, 접근 지정자 변화

학습 목표

  • “is-a vs has-a”를 예시로 구분할 수 있다.
  • Player p = k1;에서 왜 정보가 사라지는지(잘림) 설명할 수 있다.
  • Player*로 관리하면 무엇이 되고, 무엇이 안 되는지(접근 한계) 설명할 수 있다.

OOP 3대 요소

  • 상속성 / 은닉성 / 다양성(다형성) 은 면접 단골 키워드입니다.
  • 그중 상속은 “부모의 유산을 물려받는다”라기보다,
    공통 요소를 부모에 모으고, 자식은 차이만 추가하는 구조라고 이해하는 게 안전합니다.

코드 재사용 문제

  • Knight, Archer, Mage 모두 HP, Attack, Defence, Move 등 공통.
  • 반복 코드 → 수정 시 여러 곳을 동시에 수정해야 함.
  • Fight(Knight, Knight)만 있으면 타입 조합이 폭발.

상속 구조

  • Player (부모)에 공통 데이터/함수.
  • class Knight : public Player → Knight가 Player를 상속.
  • Knight에는 Knight만의 데이터 등 추가만 기술.
class Player {
public:
    void Move() {}
    void Die() {}

protected:
    int _hp;
    int _attack;
    int _defence;
};

class Mage : public Player {
public:
    int _mp;
};

int main() {
    Mage m1;
    m1._hp = 10; // Player로부터 상속받은 _hp 사용
}

실전에서는 멤버 변수를 무조건 public으로 열기보다, 최소 protected/private로 감추고(은닉),
필요한 인터페이스(함수)로만 조작하게 만드는 방향이 유지보수에 유리합니다.

is-a vs has-a

  • is-a: Knight is a Player? → Yes → 상속 OK.
  • has-a: Knight has an Inventory? → Yes → 컴포넌트로 소지 (멤버 변수). 상속 X.

is-a vs has-a 판단 도표

┌─────────────────────────────────────────────────────────────────────────────┐
│ "Knight is a Player?" → Yes  → 상속 사용 ✅                                   │
│ "Knight is an Inventory?" → No → 상속 사용 ❌                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  Is-A (상속 O)                    Has-A (상속 X, 멤버로 포함)                 │
│                                                                               │
│  class Knight : public Player     class Player {                              │
│  // Knight는 Player의 일종         Inventory _inventory;  ← 멤버 변수로 소지   │
│  // HP, Attack 등 물려받음         // Player "가진" Inventory                  │
│                                    };                                         │
│                                                                               │
│  Knight ──is_a──► Player          Player ──has_a──► Inventory                │
│  (기사는 플레이어의 일종)           (플레이어는 인벤토리를 가짐)                 │
│                                                                               │
└─────────────────────────────────────────────────────────────────────────────┘
관계설명예시
Is-A자식이 부모의 일종일 때 → 상속 적절Knight is-a Player
Has-A객체를 포함(소유)하고 있을 때 → 포함(Composition)Player has-a Inventory

부모 타입 포인터로 관리

  • Player* p1 = &k1 → 부모 타입으로 참조 가능.
  • 값 대입 (Player p1 = k1) → 잘림(object slicing). 데이터 손실.
  • 포인터/참조로 관리 → 잘림 없이 “같은 객체를 가리키기” 때문에 데이터 손실 없음.
    • 포인터 크기는 플랫폼에 따라 다릅니다(32bit=보통 4byte, 64bit=보통 8byte).

값 대입 vs 포인터 비교 도표

┌─────────────────────────────────────────────────────────────────────────────┐
│ ❌ 값 대입 (잘림 발생)                    ✅ 포인터 (참조, 손실 없음)          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  Knight k1                    Player p1              Player* p1 = &k1        │
│  ┌─────────────────┐         ┌───────────┐           ┌─────────┐             │
│  │ _hp     │ _attack│         │ _hp       │           │ 0x100   │ ──────► k1  │
│  │ _defence│_stamina│  대입   │ _attack   │  잘림!    │ (8byte) │        전체 │
│  │ (Player)│(Knight)│  ───►   │ _defence  │  _stamina │         │             │
│  └─────────────────┘         └───────────┘  없어짐   └─────────┘             │
│      큰 박스                     작은 박스               포인터는 주소만      │
│  p1 = k1  → 작은 박스에 담음   데이터 손실!            가리키므로 손실 없음   │
│                                                                               │
└─────────────────────────────────────────────────────────────────────────────┘
void Fight(Player* p1, Player* p2) {
    p1->_hp -= p2->_attack;
}

int main() {
    Knight k1;
    Mage m1;
    Fight(&k1, &m1); // 자동 업캐스팅 발생
}

⚠️ 주의:

  • Player*로는 “Player에 정의된 것만” 접근 가능합니다.
    p1->_mp 같은 Mage 전용 멤버는 접근할 수 없습니다.
  • 그래서 공통으로 다뤄야 하는 기능은 “부모(Player) 인터페이스”로 설계되어야 합니다.

상속 계층 예시

graph TB
    GameObject[GameObject]
    Creature[Creature]
    Player[Player]
    Monster[Monster]
    NPC[NPC]
    Pet[Pet]
    Projectile[Projectile]
    Arrow[Arrow]
    Fireball[Fireball]
    Item[Item]
    Weapon[Weapon]
    Sword[Sword]
    Bow[Bow]
    Armor[Armor]
    Consumable[Consumable]
    
    GameObject --> Creature
    GameObject --> Projectile
    GameObject --> Item
    
    Creature --> Player
    Creature --> Monster
    Creature --> NPC
    Creature --> Pet
    
    Projectile --> Arrow
    Projectile --> Fireball
    
    Item --> Weapon
    Item --> Armor
    Item --> Consumable
    Weapon --> Sword
    Weapon --> Bow

텍스트 계층 구조

GameObject
├─ Creature
│  ├─ Player
│  ├─ Monster
│  ├─ NPC
│  └─ Pet
├─ Projectile
│  ├─ Arrow
│  └─ Fireball
└─ Item
   ├─ Weapon
   │  ├─ Sword
   │  ├─ Bow
   │  └─ Lance
   ├─ Armor
   └─ Consumable

상속 접근 지정자 변화

상속 방식부모의 public부모의 protected부모의 private
public 상속public 유지protected 유지접근 불가
protected 상속protected로 변경protected 유지접근 불가
private 상속private로 변경private로 변경접근 불가

✅ 실무에서는 대부분 public 상속 사용.

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

  • Player p = k1;에서 “잘림”이 일어나는 이유는?
  • is-a가 아닌데 상속으로 만들면 어떤 문제가 생길까?
  • 부모 포인터(Player*)로 관리할 때, 자식 고유 기능을 바로 쓰기 어려운 이유는?

profile
李家네_공부방

0개의 댓글