본문 바로가기
카테고리 없음

[C++] 가상 함수 (virtual) 사용법 완벽 정리 및 예제

by 정보모아모아 2025. 1. 2.

C++에서 가상 함수 (virtual function)는 다형성을 구현하기 위한 핵심 기능입니다. 이를 통해 부모 클래스의 포인터나 참조를 사용하여 파생 클래스의 메서드를 호출할 수 있습니다. 이 글에서는 가상 함수의 개념, 사용법, 그리고 다양한 예제를 통해 이를 완벽히 이해해보겠습니다.


가상 함수란?

가상 함수는 기본 클래스에서 선언되고, 파생 클래스에서 재정의(override) 될 수 있는 함수입니다. 

 

여러분은 RPG 게임을 개발 중이고, 플레이어는 전사, 마법사, 도적 중 하나의 직업을 선택할 수 있습니다. 각 직업은 고유한 스킬을 가지고 있습니다.

 

다음과 같은 대상들이 있다고 가정해보고, 이를 가상 함수에 대응해서 설명해볼게요.

  • 플레이어(함수 호출): 플레이어의 행동, 즉 스킬 사용 명령을 내리는 주체입니다.
  • 캐릭터(객체): 전사, 마법사, 도적과 같이 게임 내에서 플레이어가 조종하는 주체입니다. 각각 다른 능력을 가지고 있습니다. (상속 관계에 있는 여러 클래스의 객체들)
  • 스킬(함수): 캐릭터가 수행할 수 있는 행동, 즉 공격, 방어, 특수 능력 등입니다. (기반 클래스에 정의된 함수)
  • 기본 스킬(비가상 함수, 일반적인 함수): 모든 캐릭터가 공통적으로 사용할 수 있는 일반적인 스킬입니다. 예를 들어, "기본 공격"과 같이 모든 직업이 사용할 수 있는 평범한 공격입니다.
  • 고유 스킬(가상 함수): 각 캐릭터 직업별로 특화된 고유한 스킬입니다. 예를 들어, 전사는 "강력한 일격", 마법사는 "파이어볼", 도적은 "은신 후 기습"과 같은 특별한 기술을 사용할 수 있습니다. (파생 클래스에서 재정의된 함수)

이제 게임에서 다음과 같은 상황을 상상해 봅시다.

  1. 일반 함수(비가상 함수) 사용:
    • 플레이어가 어떤 캐릭터를 선택하든, "기본 공격" 명령을 내리면, 모든 캐릭터는 정해진 기본 공격 모션과 피해량을 출력합니다. 즉, 캐릭터의 직업과 상관없이 동일한 "기본 공격" 함수가 실행됩니다.
  2. 가상 함수(고유 스킬) 사용:
    • 플레이어가 "스킬 사용" 명령을 내립니다.
    • 시스템은 플레이어가 현재 조종하고 있는 캐릭터의 직업을 확인합니다. (런타임에 객체의 타입을 확인하는 단계)
    • 확인된 직업에 맞는 고유 스킬 함수를 호출합니다.
      1. 전사를 조종하고 있다면, "강력한 일격" 함수가 호출되어, 전사 특유의 강력한 공격 모션과 피해량을 출력합니다.
      2. 마법사를 조종하고 있다면, "파이어볼" 함수가 호출되어, 마법사 특유의 화려한 마법 공격 모션과 피해량을 출력합니다.
      3. 도적을 조종하고 있다면, "은신 후 기습" 함수가 호출되어, 도적 특유의 은밀한 기습 공격 모션과 피해량을 출력합니다.
    • 이것이 C++에서 가상 함수가 작동하는 방식입니다. 런타임에 객체의 실제 타입을 확인하고, 해당 타입에 맞게 재정의된 함수(고유 스킬)가 있으면 그 함수를 호출합니다. 만약 재정의된 함수가 없으면, 기반 클래스에 정의된 함수(기본 스킬)를 호출합니다.

핵심:

  • 가상 함수를 사용하면, 플레이어(함수 호출)는 동일한 "스킬 사용" 명령을 내리더라도, 캐릭터의 직업(객체의 타입)에 맞는 고유 스킬(함수)을 실행시킬 수 있습니다.
  • 즉, 어떤 스킬(함수)이 실행될지는 런타임에 캐릭터의 직업(객체의 타입)에 따라 동적으로 결정됩니다. 이를 동적 바인딩, 또는 런타임 다형성이라고 합니다.

가상 함수 선언 및 사용법

 

기본 예제: 가상 함수와 재정의

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() {
        cout << "Base 클래스" << endl;
    }
};

class Derived : public Base {
public:
    void display() override { // override는 선택적
        cout << "Derived 클래스" << endl;
    }
};

int main() {
    Base* basePtr;
    Derived derivedObj;

    basePtr = &derivedObj;
    basePtr->display(); // Derived 클래스 호출

    return 0;
}

실행 결과 : 파생(Derived) 클래스의 함수가 실행됨. 

Derived 클래스

설명:

  • Base* basePtr = &derivedObj; //부모 클래스 포인터로 파생 클래스 객체를 가리킵니다.
  • basePtr->display(); //파생 클래스의 display()를 호출합니다.

가상 함수와 소멸자

소멸자는 가상 함수로 선언하지 않으면, 부모 클래스 포인터를 사용해 파생 클래스 객체를 삭제할 때 메모리 누수가 발생할 수 있습니다.

예제: 가상 소멸자

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() {
        cout << "Base 소멸자" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived 소멸자" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    delete basePtr; // Derived와 Base 소멸자 모두 호출
    return 0;
}

실행 결과:

Derived 소멸자
Base 소멸자

설명:

  • virtual 키워드가 없으면 Base 소멸자만 호출됩니다.
  • virtual을 사용하면 모든 소멸자가 올바르게 호출됩니다.

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

순수 가상 함수는 서브클래스에서 반드시 구현해야 하는 함수입니다. 이를 통해 추상 클래스(인터페이스와 유사한 개념)를 정의할 수 있습니다.

 

예제: 순수 가상 함수

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0; // 순수 가상 함수
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "원을 그립니다." << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "사각형을 그립니다." << endl;
    }
};

int main() {
    Shape* shape;
    Circle circle;
    Rectangle rectangle;

    shape = &circle;
    shape->draw(); // 원을 그립니다.

    shape = &rectangle;
    shape->draw(); // 사각형을 그립니다.

    return 0;
}

실행 결과:

원을 그립니다.
사각형을 그립니다.

설명:

  • Shape 클래스는 추상 클래스입니다.
  • draw() 메서드는 Circle과 Rectangle에서 구현됩니다.

가상 함수 테이블(Virtual Table, V-Table)

가상 함수 호출은 가상 함수 테이블(V-Table)을 사용하여 동작합니다. 

동작 원리:

  1. 클래스에 가상 함수가 존재하면 컴파일러는 V-Table을 생성.
  2. 각 객체는 V-Table에 대한 포인터(VPTR)를 가집니다.
  3. 가상 함수 호출 시 VPTR을 통해 올바른 함수가 실행됩니다.

주의:

  • 가상 함수는 일반 함수보다 약간의 성능 오버헤드가 있습니다.
  • 다형성이 필요한 경우에만 사용하세요.

정리

가상 함수는 코드의 유연성과 확장성을 높여줍니다. 새로운 직업이 추가되더라도, "스킬 사용"이라는 인터페이스는 유지하면서 각 직업의 고유한 스킬을 쉽게 구현할 수 있습니다. 플레이어는 어떤 캐릭터를 선택하든 동일한 명령어로 다양한 스킬을 사용할 수 있어, 게임 플레이가 더욱 풍부해집니다.