이 글에서는 C++의 가상 함수(Virtual Function)가 무엇인지, 그리고 어떻게 동작하는지 예제 코드를 통해 자세히 알아보겠습니다.
가상 함수란?
가상 함수는 기본 클래스(base class)에서 선언되고, 파생 클래스(derived class)에서 재정의(overriding)할 수 있는 멤버 함수입니다. 함수 선언 앞에 virtual 키워드를 붙여 표시합니다.
가상 함수의 핵심은 런타임 다형성(runtime polymorphism)을 구현한다는 점입니다. 기본 클래스 포인터로 파생 클래스 객체를 가리킬 때, 가상 함수는 실제 객체의 타입에 맞는 함수가 호출되지만, 일반 함수는 포인터 타입(기본 클래스)의 함수가 호출됩니다.
예제 코드
#include <iostream>
using namespace std;
class base {
public:
// 가상 함수로 선언
virtual void print(){
cout << "print base class" << endl;
}
// 일반(비가상) 함수
void show(){
cout << "show base class" << endl;
}
};
class derived : public base {
public:
void print(){
cout << "print derived class" << endl;
}
void show(){
cout << "show derived class" << endl;
}
};
int main(){
base* bptr; // 기본 클래스 포인터
derived d; // 파생 클래스 객체
bptr = &d; // 포인터가 파생 클래스 객체를 가리킴
// 가상 함수 호출 → 실제 객체 타입 기준으로 호출됨
bptr->print();
// 비가상 함수 호출 → 포인터 타입 기준으로 호출됨
bptr->show();
}실행 결과
print derived class show base class
결과 분석
실행 결과가 흥미롭습니다. 같은 기본 클래스 포인터(bptr)를 통해 호출했는데도 두 함수의 동작이 다릅니다.
print():virtual로 선언되었기 때문에, 컴파일러가 런타임에 실제 객체의 타입(derived)을 확인하여 파생 클래스의 함수를 호출합니다. 이것이 바로 동적 바인딩(dynamic binding)입니다.show(): 가상 함수가 아니므로 컴파일 시점에 포인터 타입(base*)을 기준으로 정적 바인딩되어, 기본 클래스의 함수가 호출됩니다.
가상 함수의 내부 동작 원리
C++ 컴파일러는 가상 함수가 하나라도 있는 클래스에 대해 가상 함수 테이블(vtable)을 생성하고, 각 객체에는 이 테이블을 가리키는 vptr이 숨겨져 있습니다. 함수 호출 시 이 테이블을 참조해 실제 객체 타입에 맞는 함수 주소를 찾아 실행하는 방식으로 다형성이 구현됩니다.
참고: C++11의 override 키워드
C++11부터는 파생 클래스에서 함수를 재정의할 때 override 키워드를 붙이는 것이 권장됩니다. 부모 클래스에 해당 가상 함수가 없거나 시그니처가 일치하지 않으면 컴파일 오류가 발생하므로, 실수를 조기에 잡을 수 있습니다.
class derived : public base {
public:
void print() override { // 재정의임을 명시적으로 표시
cout << "print derived class" << endl;
}
};정리
가상 함수는 기본 클래스 포인터나 참조를 통해 파생 클래스의 재정의된 함수를 호출할 수 있게 해주는 C++ 다형성의 핵심 도구입니다. 객체 지향 설계에서 인터페이스 추상화와 유연한 확장성을 위해 반드시 이해하고 활용해야 할 중요한 개념입니다.