C++에서 가상 함수(Virtual Function)와 순수 가상 함수(Pure Virtual Function)는 다형성(polymorphism)을 구현하는 핵심 도구입니다. 두 개념은 비슷해 보이지만 선언 방식과 용도, 파생 클래스에 미치는 영향에서 중요한 차이가 있습니다. 아래 표는 두 함수의 주요 차이점을 한눈에 비교한 것입니다.
| 가상 함수 (Virtual Function) | 순수 가상 함수 (Pure Virtual Function) |
|---|---|
| 클래스 내부에 실제 구현(정의)을 가집니다. | 선언만 존재하며 별도의 구현(정의)이 없습니다. |
선언 형태: virtual funct_name(parameter_list) { ... }; | 선언 형태: virtual funct_name(parameter_list) = 0; |
| 파생 클래스에서 반드시 재정의할 필요는 없습니다. | 클래스에 순수 가상 함수가 하나라도 포함되면 해당 클래스는 추상 클래스(abstract class)로 선언되어 인스턴스를 생성할 수 없습니다. |
| 필요에 따라 기본 클래스에서 제공한 구현을 그대로 사용하거나 재정의(오버라이드)할 수 있습니다. | 파생 클래스는 반드시 순수 가상 함수를 재정의해야만 객체 생성이 가능합니다. |
가상 함수(Virtual Function)란?
가상 함수는 기본 클래스에서 virtual 키워드로 선언되는 함수로, 기본 클래스 포인터나 참조를 통해 호출될 때 런타임에 실제 객체의 타입에 맞는 함수가 실행되도록 합니다. 이것이 바로 런타임 다형성의 원리입니다. 파생 클래스에서 재정의할 수 있지만 필수 사항은 아니며, 재정의하지 않으면 기본 클래스의 구현이 그대로 사용됩니다.
예제 코드
#include <iostream>
using namespace std;
class B {
public:
virtual void s() { // 가상 함수
cout << " In Base \n";
}
};
class D : public B {
public:
void s() {
cout << "In Derived \n";
}
};
int main(void) {
D d; // D 클래스의 객체
B *b = &d; // d를 가리키는 B* 타입의 포인터
b->s(); // D::s()가 호출됨
return 0;
}
실행 결과
In Derived
B 타입의 포인터가 D 객체를 가리키고 있음에도 불구하고, s()가 가상 함수로 선언되었기 때문에 컴파일러가 런타임에 실제 객체 타입(D)의 s()를 호출합니다. 만약 virtual 키워드가 없었다면 "In Base"가 출력되었을 것입니다.
순수 가상 함수(Pure Virtual Function)란?
순수 가상 함수는 구현부 없이 선언만 존재하며, 끝에 = 0을 붙여 표기하는 가상 함수입니다. 순수 가상 함수를 하나라도 포함하는 클래스는 추상 클래스가 되어 직접 객체를 생성할 수 없으며, 파생 클래스가 반드시 구현해야 할 인터페이스를 정의하는 역할을 합니다.
예제 코드
#include <iostream>
using namespace std;
class B {
public:
virtual void s() = 0; // 순수 가상 함수
};
class D : public B {
public:
void s() {
cout << " Virtual Function in Derived class\n";
}
};
int main() {
B *b;
D dobj; // D 클래스의 객체
b = &dobj; // dobj를 가리키는 B* 타입의 포인터
b->s(); // D::s()가 호출됨
}
실행 결과
Virtual Function in Derived class
기본 클래스 B는 순수 가상 함수만을 가지므로 추상 클래스이며, B obj;처럼 객체를 생성하려 하면 컴파일 오류가 발생합니다. 파생 클래스 D가 s()를 구현했기 때문에 비로소 객체 생성과 다형적 호출이 가능해집니다.
정리
가상 함수는 기본 구현을 제공하면서 파생 클래스에서 선택적으로 재정의할 수 있는 반면, 순수 가상 함수는 구현 없이 인터페이스 역할만 수행하며 파생 클래스의 구현을 강제합니다. 따라서 공통 동작을 기본으로 제공해야 한다면 가상 함수를, 파생 클래스마다 반드시 서로 다르게 구현해야 하는 기능이라면 순수 가상 함수를 사용하는 것이 바람직한 설계입니다.