C++에서 가상 함수 (virtual function)는 다형성을 구현하기 위한 핵심 기능입니다. 이를 통해 부모 클래스의 포인터나 참조를 사용하여 파생 클래스의 메서드를 호출할 수 있습니다. 이 글에서는 가상 함수의 개념, 사용법, 그리고 다양한 예제를 통해 이를 완벽히 이해해보겠습니다.
가상 함수란?
가상 함수는 기본 클래스에서 선언되고, 파생 클래스에서 재정의(override) 될 수 있는 함수입니다.
여러분은 RPG 게임을 개발 중이고, 플레이어는 전사, 마법사, 도적 중 하나의 직업을 선택할 수 있습니다. 각 직업은 고유한 스킬을 가지고 있습니다.
다음과 같은 대상들이 있다고 가정해보고, 이를 가상 함수에 대응해서 설명해볼게요.
- 플레이어(함수 호출): 플레이어의 행동, 즉 스킬 사용 명령을 내리는 주체입니다.
- 캐릭터(객체): 전사, 마법사, 도적과 같이 게임 내에서 플레이어가 조종하는 주체입니다. 각각 다른 능력을 가지고 있습니다. (상속 관계에 있는 여러 클래스의 객체들)
- 스킬(함수): 캐릭터가 수행할 수 있는 행동, 즉 공격, 방어, 특수 능력 등입니다. (기반 클래스에 정의된 함수)
- 기본 스킬(비가상 함수, 일반적인 함수): 모든 캐릭터가 공통적으로 사용할 수 있는 일반적인 스킬입니다. 예를 들어, "기본 공격"과 같이 모든 직업이 사용할 수 있는 평범한 공격입니다.
- 고유 스킬(가상 함수): 각 캐릭터 직업별로 특화된 고유한 스킬입니다. 예를 들어, 전사는 "강력한 일격", 마법사는 "파이어볼", 도적은 "은신 후 기습"과 같은 특별한 기술을 사용할 수 있습니다. (파생 클래스에서 재정의된 함수)
이제 게임에서 다음과 같은 상황을 상상해 봅시다.
- 일반 함수(비가상 함수) 사용:
- 플레이어가 어떤 캐릭터를 선택하든, "기본 공격" 명령을 내리면, 모든 캐릭터는 정해진 기본 공격 모션과 피해량을 출력합니다. 즉, 캐릭터의 직업과 상관없이 동일한 "기본 공격" 함수가 실행됩니다.
- 가상 함수(고유 스킬) 사용:
- 플레이어가 "스킬 사용" 명령을 내립니다.
- 시스템은 플레이어가 현재 조종하고 있는 캐릭터의 직업을 확인합니다. (런타임에 객체의 타입을 확인하는 단계)
- 확인된 직업에 맞는 고유 스킬 함수를 호출합니다.
- 전사를 조종하고 있다면, "강력한 일격" 함수가 호출되어, 전사 특유의 강력한 공격 모션과 피해량을 출력합니다.
- 마법사를 조종하고 있다면, "파이어볼" 함수가 호출되어, 마법사 특유의 화려한 마법 공격 모션과 피해량을 출력합니다.
- 도적을 조종하고 있다면, "은신 후 기습" 함수가 호출되어, 도적 특유의 은밀한 기습 공격 모션과 피해량을 출력합니다.
- 이것이 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)을 사용하여 동작합니다.
동작 원리:
- 클래스에 가상 함수가 존재하면 컴파일러는 V-Table을 생성.
- 각 객체는 V-Table에 대한 포인터(VPTR)를 가집니다.
- 가상 함수 호출 시 VPTR을 통해 올바른 함수가 실행됩니다.
주의:
- 가상 함수는 일반 함수보다 약간의 성능 오버헤드가 있습니다.
- 다형성이 필요한 경우에만 사용하세요.
정리
가상 함수는 코드의 유연성과 확장성을 높여줍니다. 새로운 직업이 추가되더라도, "스킬 사용"이라는 인터페이스는 유지하면서 각 직업의 고유한 스킬을 쉽게 구현할 수 있습니다. 플레이어는 어떤 캐릭터를 선택하든 동일한 명령어로 다양한 스킬을 사용할 수 있어, 게임 플레이가 더욱 풍부해집니다.