본문 바로가기

가상 함수와 순수 가상 함수

@iamrain2025. 9. 2. 12:18

1. 이론

1.1. 다형성(Polymorphism)과 동적 바인딩(Dynamic Binding)

C++에서 다형성은 "하나의 인터페이스가 다양한 타입의 객체에 대해 다르게 동작"하는 것을 의미한다. 다형성을 구현하는 핵심 메커니즘이 바로 동적 바인딩이다.

  • 정적 바인딩 (Static Binding): 컴파일 시점에 어떤 함수가 호출될지 결정된다. 일반적인 함수 호출이 여기에 해당.
  • 동적 바인딩 (Dynamic Binding): 런타임 시점에 객체의 실제 타입에 따라 어떤 함수가 호출될지 결정된다. 가상 함수를 통해 이루어지며, 포인터나 참조를 통해 객체에 접근할 때 동작한다.

동적 바인딩은 가상 테이블(Virtual Table, vtable) 이라는 메커니즘을 통해 구현된다.

  • vtable: 가상 함수를 하나 이상 가진 클래스에 대해, 컴파일러는 해당 클래스의 가상 함수들의 주소를 담고 있는 테이블을 생성한다. 이 테이블은 클래스 단위로 하나만 존재.
  • vptr (virtual pointer): 가상 함수를 가진 클래스의 객체가 생성될 때, 객체의 메모리 공간 앞부분에 숨겨진 포인터(vptr)가 추가된다. 이 포인터는 자기 클래스의 vtable을 가리킨다.

함수 호출 시, 컴파일러는 vptr을 통해 vtable에 접근하고, vtable에서 호출할 함수의 주소를 찾아 실행한다. 이 과정이 런타임에 일어나므로 동적 바인딩이 가능해진다.


1.2. 가상 함수 (Virtual Function)

"이 함수는 파생 클래스에서 재정의(Override)될 수 있습니다."

가상 함수는 virtual 키워드를 사용하여 선언하며, 파생 클래스에서 재정의될 것을 기대하는 함수다. 기반 클래스의 포인터나 참조가 파생 클래스의 객체를 가리킬 때, 가상 함수를 호출하면 실제 객체의 타입에 맞는 재정의된 함수가 호출된다.

  • 특징
    • 기반 클래스에서 기본 구현(Default Implementation)을 제공할 수 있다.
    • 파생 클래스는 필요에 따라 이 함수를 재정의할 수도, 하지 않을 수도 있다. 재정의하지 않으면 기반 클래스의 함수가 호출된다.
  • 가상 소멸자 (Virtual Destructor)
    • 기반 클래스의 포인터로 파생 클래스 객체를 delete할 때, 소멸자가 virtual이 아니면 기반 클래스의 소멸자만 호출되어 메모리 누수가 발생한다.
    • 따라서 다형성을 사용하는 클래스 계층 구조에서는 반드시 기반 클래스의 소멸자를 virtual로 선언해야 한다.
class Base {
public:
    virtual void myFunction() { /* 기본 구현 */ }
    virtual ~Base() {} // 가상 소멸자
};

1.3. 순수 가상 함수 (Pure Virtual Function)

"이 함수는 파생 클래스에서 반드시 재정의되어야 합니다."

순수 가상 함수는 함수 선언부 끝에 = 0;을 붙여 선언한다. 순수 가상 함수는 본문(구현)을 가지지 않으며, 이 함수를 하나 이상 포함하는 클래스는 추상 클래스(Abstract Class)가 된다.

  • 특징:
    • 추상 클래스는 인스턴스화(객체 생성)할 수 없다. new MyAbstractClass(); 와 같은 코드는 컴파일 에러를 발생시킨다.
    • 오직 파생 클래스를 위한 인터페이스(Interface) 명세 역할을 한다.
    • 파생 클래스는 기반 클래스의 모든 순수 가상 함수를 재정의해야만 일반 클래스가 되어 객체를 생성할 수 있다. 하나라도 재정의하지 않으면 해당 파생 클래스 또한 추상 클래스가 된다.
class AbstractBase {
public:
    virtual void mustBeImplemented() = 0; // 순수 가상 함수
    virtual ~AbstractBase() {}
};

1.4. 비교 및 사용 사례

구분 가상 함수 (Virtual Function) 순수 가상 함수 (Pure Virtual Function)
선언 virtual void func(); virtual void func() = 0;
기반 클래스 구현 구현을 가질 수 있음 (선택) 구현을 가지지 않음 (인터페이스 역할)
파생 클래스 재정의 선택 사항 필수 사항
기반 클래스 객체 생성 가능 불가능 (추상 클래스)
주요 목적 기본 동작을 제공하되, 확장/변경의 여지를 둠 파생 클래스가 반드시 구현해야 할 기능의 규격(Interface)을 강제

언제 무엇을 사용할까?

  • 가상 함수:
    • "대부분의 파생 클래스는 이 동작을 그대로 사용하지만, 몇몇 특별한 클래스는 다르게 동작해야 해."
    • 예: GameObjectonCollision() 함수. 기본적으로는 아무 일도 하지 않지만(virtual void onCollision(){}), PlayerItem 객체는 특별한 처리를 하도록 재정의할 수 있다.
  • 순수 가상 함수:
    • "이 클래스 계열의 모든 객체는 반드시 이 기능을 가져야 하지만, 그 내용은 각자 알아서 정해야 해."
    • 예: IGameObjectupdate()render() 함수. 세상의 모든 게임 오브젝트는 매 프레임 update되고 render되어야 하지만, PlayerupdateEnemyupdate는 완전히 다르다. 기반 클래스에서 update를 어떻게 구현해야 할지 정의할 수 없으므로, 순수 가상 함수로 만들어 파생 클래스에게 구현을 강제한다.

 

2. 실습 코드 (게임 예제)

아래는 게임 월드에 존재하는 다양한 오브젝트(Player, Monster, Prop)를 다형적으로 관리하는 예제다.

  • IGameObject: 모든 게임 오브젝트가 따라야 할 인터페이스다. (추상 클래스).
  • update(): 매 프레임 오브젝트의 상태를 갱신한다. (순수 가상 함수)
  • render(): 매 프레임 오브젝트를 화면에 그린다. (순수 가상 함수)
  • onCollision(): 다른 오브젝트와 충돌했을 때의 반응이다. (가상 함수)
  • isDestroyed() / destroy(): 오브젝트의 파괴 상태를 관리한다.
#include <iostream>
#include <vector>
#include <string>
#include <memory>

// --- 인터페이스 정의 ---

/* Q1. 아래 클래스의 역할과 '순수 가상 소멸자'가 필요한 이유를 설명하세요.
 * A. 역할 : 게임 내 모든 오브젝트의 공통 인터페이스 역할. 기본 동작을 정의함.
 * 순수 가상 소멸자가 필요한 이유 : 가상 소멸자를 선언하지 않으면 기반 클래스 포인터로
 * 파생 클래스 객체를 삭제할 때 파생 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생함.
 */
class IGameObject {
public:
    virtual ~IGameObject() = default;

    /* Q2. 아래 두 함수가 '순수 가상 함수'인 이유를 설명하세요.
     * A. update : 오브젝트 상태를 매 프레임 갱신해야 하지만 어떤 갱신 로직을 쓸지는
     *  객체마다 다름. 즉, 추상화가 필요함.
     *  render : 화면의 그리는 방식은 객체마다 다르므로 파생 클래스에서 재정의가 필요함.
     */
    virtual void update() = 0;
    virtual void render() const = 0;

    /* Q3. 아래 함수가 '순수 가상 함수'가 아닌 '가상 함수'인 이유를 설명하세요.
     * A. 충돌은 일어날 수도 있고, 안 일어날 수도 있음. 즉 필요한 객체만 구현하면 됨.
     */
    virtual void onCollision(IGameObject* other) {
        // 기본적으로는 아무런 충돌 처리도 하지 않음
    }

    // 오브젝트의 생존 상태를 관리하는 일반 함수들
    bool isDestroyed() const { return m_isDestroyed; }
    void destroy() { m_isDestroyed = true; }
    const std::string& getName() const { return m_name; }

protected:
    // 생성자를 protected로 선언하여 외부에서 IGameObject 단독으로 생성하는 것을 막음
    IGameObject(std::string name) : m_name(std::move(name)), m_isDestroyed(false) {}

private:
    std::string m_name;
    bool m_isDestroyed;
};


// --- 구체적인 오브젝트 구현 ---

// Player 클래스
class Player : public IGameObject {
public:
    Player(std::string name, int hp) : IGameObject(std::move(name)), m_hp(hp) {
        std::cout << "플레이어 '" << getName() << "' 생성 (HP: " << m_hp << ")" << std::endl;
    }

    // IGameObject의 순수 가상 함수들을 반드시 구현해야 함
    void update() override {
        // 플레이어는 매 프레임 사용자 입력을 받거나, 자동으로 체력을 회복할 수 있음
        m_hp += 1; // 매 프레임 체력 1 회복한다고 가정
        std::cout << "플레이어 '" << getName() << "' 업데이트! (현재 HP: " << m_hp << ")" << std::endl;
    }

    void render() const override {
        std::cout << "  -> 화면에 플레이어 '" << getName() << "'를 그립니다." << std::endl;
    }

    // Player는 충돌 시 특별한 반응을 하므로 onCollision을 재정의
    void onCollision(IGameObject* other) override {
        std::cout << "플레이어 '" << getName() << "'가 '" << other->getName() << "'와 충돌!" << std::endl;
        // 만약 몬스터와 충돌했다면 데미지를 입음
        m_hp -= 10;
        if (m_hp <= 0) {
            std::cout << "플레이어 '" << getName() << "'가 파괴되었습니다..." << std::endl;
            destroy();
        }
    }

private:
    int m_hp;
};

// Monster 클래스
class Monster : public IGameObject {
public:
    Monster(std::string name, int attackPower) : IGameObject(std::move(name)), m_attackPower(attackPower) {
        std::cout << "몬스터 '" << getName() << "' 생성 (공격력: " << m_attackPower << ")" << std::endl;
    }

    void update() override {
        // 몬스터는 매 프레임 주변을 배회하거나 플레이어를 추적하는 AI를 실행할 수 있음
        std::cout << "몬스터 '" << getName() << "' 업데이트! (플레이어를 찾고 있습니다...)" << std::endl;
    }

    void render() const override {
        std::cout << "  -> 화면에 몬스터 '" << getName() << "'를 그립니다." << std::endl;
    }

    // 몬스터는 충돌 시 별다른 반응이 없으므로 onCollision을 재정의하지 않음
    // 따라서 IGameObject의 기본 onCollision이 호출됨

private:
    int m_attackPower;
};

// Prop(배경 소품) 클래스
class Prop : public IGameObject {
public:
    Prop(std::string name) : IGameObject(std::move(name)) {
        std::cout << "배경 소품 '" << getName() << "' 생성" << std::endl;
    }

    void update() override {
        // 배경 소품은 보통 매 프레임 상태가 변하지 않음
    }

    void render() const override {
        std::cout << "  -> 화면에 배경 소품 '" << getName() << "'을 그립니다." << std::endl;
    }
};


// --- 게임 월드 및 메인 루프 ---

int main() {
    // 게임 월드에 존재하는 모든 오브젝트를 관리하는 벡터
    // 스마트 포인터를 사용하여 메모리를 안전하게 관리
    std::vector<std::unique_ptr<IGameObject>> gameObjects;

    // 월드에 오브젝트 추가
    gameObjects.emplace_back(std::make_unique<Player>("용사", 100));
    gameObjects.emplace_back(std::make_unique<Monster>("고블린", 10));
    gameObjects.emplace_back(std::make_unique<Prop>("나무"));
    gameObjects.emplace_back(std::make_unique<Monster>("오크", 25));

    std::cout << "--- 게임 루프 시작! ---" << std::endl;

    // 3 프레임 동안 게임 루프 실행
    for (int frame = 1; frame <= 3; ++frame) {
        std::cout << "--- 프레임 " << frame << " ---";

        // 1. 업데이트: 모든 오브젝트의 상태를 갱신
        for (const auto& obj : gameObjects) {
            /* Q4. 아래 obj->update() 호출이 어떻게 각기 다른 클래스의 update를 실행시키는지 설명하세요. (동적 바인딩)
             * A. obj는 항상 IGameObject* 타입을 가리킴. update는 virtual로 선언. 즉, vtable을 사용해
             *  런타임에 실제 객체 타입에 맞는 함수가 호출됨.
             */
            obj->update();
        }

        // 2. 충돌 처리 (예시: 플레이어와 첫 번째 몬스터가 충돌했다고 가정)
        if (frame == 2) {
            std::cout << "*이벤트 발생: 플레이어와 고블린 충돌!*";
            gameObjects[0]->onCollision(gameObjects[1].get()); // Player의 onCollision 호출
            gameObjects[1]->onCollision(gameObjects[0].get()); // Monster의 onCollision (기반 클래스 버전) 호출
            std::cout << std::endl;
        }

        // 3. 렌더링: 모든 오브젝트를 화면에 그림
        for (const auto& obj : gameObjects) {
            obj->render();
        }

        // 4. 파괴된 오브젝트 제거 (실제 게임에서는 더 복잡한 방식 사용)
        // 여기서는 간단히 루프 종료

        std::cout << std::endl;
    }

    std::cout << "--- 게임 루프 종료! ---" << std::endl;

    // unique_ptr을 사용했으므로 main 함수가 끝나면 자동으로 메모리가 해제됨
    // 이 때 각 객체의 소멸자가 호출되고, IGameObject의 가상 소멸자 덕분에
    // Player, Monster, Prop의 소멸자가 모두 안전하게 호출됨

    return 0;
}

'C++' 카테고리의 다른 글

구조체  (0) 2025.09.03
클래스  (0) 2025.09.02
템플릿  (2) 2025.09.01
malloc/free 와 new/delete  (1) 2025.08.26
가상 소멸자 Virtual Destructor  (0) 2025.08.25
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차