다형성 (Polymorphism)

Jaemyeong Lee·2024년 12월 6일

게임 서버1

목록 보기
42/220

이 Step에서 다루는 것

  • “부모 타입 하나로 여러 자식 타입을 다루는 방법” = 다형성
  • 오버로딩/오버라이딩 차이
  • virtual이 없을 때(정적 바인딩)와 있을 때(동적 바인딩)의 차이를 코드로 증명
  • VTable/VPtr이 왜 생기는지(감각)
  • 상속에서 virtual 소멸자가 왜 필수 규칙인지
  • 순수 가상 함수로 “인터페이스(추상 클래스)”를 만드는 이유

학습 목표

  • Player* p = &k1; p->Move();가 왜 Knight의 Move를 호출하게 만들 수 있는지 설명할 수 있다.
  • “부모 포인터로 delete하면 virtual 소멸자가 필요한 이유”를 설명할 수 있다.
  • 오버로딩과 오버라이딩을 예시로 구분할 수 있다.

오버로딩 vs 오버라이딩

종류설명예시
오버로딩(Overloading)같은 이름, 다른 매개변수(시그니처). 함수 이름 재사용void Print(int); void Print(double);
오버라이딩(Overriding)부모의 가상 함수(virtual)를 자식에서 재정의void Move() override

정적 바인딩 vs 동적 바인딩 (핵심)

구분결정 시점언제 발생?결과
정적 바인딩컴파일 시점virtual 없음“표현식의 타입” 기준으로 결정
동적 바인딩실행 시점virtual 있음“실제 객체 타입” 기준으로 결정

코드로 증명해보면 훨씬 빨리 이해됩니다.

#include <iostream>

class Player {
public:
    void Move() { std::cout << "Player::Move\n"; }          // non-virtual
    virtual void VMove() { std::cout << "Player::VMove\n"; } // virtual
    virtual ~Player() = default;
};

class Knight : public Player {
public:
    void Move() { std::cout << "Knight::Move\n"; }      // (이름은 같아도) 다형성 아님
    void VMove() override { std::cout << "Knight::VMove\n"; } // 다형성
};

int main()
{
    Knight k;
    Player* p = &k;

    p->Move();   // 정적 바인딩: Player::Move
    p->VMove();  // 동적 바인딩: Knight::VMove
}

핵심 결론:

  • “부모 포인터로 호출했을 때 자식 함수를 자동으로 호출”하려면 virtual이 필요합니다.
  • 이름만 같고 virtual이 아니면, 사실상 “부모/자식 각각의 다른 함수”일 뿐입니다.

virtual + override (실전 기본 세트)

  • 부모에서 virtual을 붙이고
  • 자식에서 override를 붙이는 게 실전에서 가장 안전합니다.
class Shape {
public:
    virtual void Draw() { std::cout << "Shape\n"; }
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    void Draw() override { std::cout << "Circle\n"; }
};

override를 붙이면 “부모에 같은 시그니처의 virtual이 없을 때” 컴파일 에러가 나서 실수를 막아줍니다.


가상 함수 테이블 (VTable) 감각

  • virtual 함수가 있으면, 객체는 보통 “가상 함수 테이블을 가리키는 포인터(vptr)”를 하나 갖습니다.
  • 호출 흐름(감각):
    • p->VMove()를 호출하면
    • p가 가리키는 객체의 vptr로 vtable을 찾고
    • 거기서 “해당 함수 자리”의 실제 함수 주소를 꺼내 호출합니다.

vptr 크기는 “포인터 크기”라서 플랫폼에 따라 달라집니다(32/64bit).


(필수 규칙) 부모 소멸자는 virtual

상속 구조에서 부모 포인터로 delete 할 가능성이 있다면,
부모 소멸자는 반드시 virtual이어야 합니다.

#include <iostream>
#include <memory>

class Shape {
public:
    virtual ~Shape() { std::cout << "~Shape\n"; }
};

class Circle : public Shape {
public:
    ~Circle() override { std::cout << "~Circle\n"; }
};

int main()
{
    std::unique_ptr<Shape> s = std::make_unique<Circle>();
} // ~Circle → ~Shape 순서로 호출됨

왜 필요하나?

  • Shape* p = new Circle(); delete p; 같은 코드에서
  • virtual 소멸자가 아니면 자식 소멸자가 호출되지 않을 수 있어 자원 누수/미정리가 발생합니다.

순수 가상 함수 · 추상 클래스

  • = 0은 “구현이 없다(자식이 반드시 구현해야 한다)”는 의미입니다.
  • 순수 가상 함수가 하나라도 있으면 그 클래스는 추상 클래스가 되고,
    단독으로 객체 생성이 불가능합니다.
class Shape {
public:
    virtual void Draw() = 0; // 순수 가상 함수
    virtual ~Shape() = default;
};

추상 클래스의 목적:

  • “공통 인터페이스(규칙)”를 강제하고
  • 구체 구현은 자식에게 맡기기 위함

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

  • p->Move()p->VMove()가 다른 결과가 나오는 이유는?
  • 부모 포인터로 delete할 수 있는 구조에서 부모 소멸자는 왜 virtual이어야 할까?
  • 오버로딩과 오버라이딩은 무엇이 다른가?

profile
李家네_공부방

0개의 댓글