순수 가상 함수와 추상 클래스
순수 가상 함수와 추상 클래스
객체를 만들 수 없으면서도 기반 클래스(base class)로는 쓸 수 있는 타입을 '추상 타입'이라 불러요. 이 페이지에서는 그 추상 타입을 만드는 순수 가상 함수(pure virtual function)와, 순수 가상 함수를 하나라도 가진 추상 클래스(abstract class)가 정확히 뭘 의미하는지 규칙 기준으로 정리해 볼게요.
출처: cppreference
본문
순수 가상 함수는, 선언자(declarator)에 다음 문법을 가진 가상 함수예요.
declarator virt-specifier (optional) = 0
여기서 = 0 시퀀스를 pure-specifier라고 불러요. 선언자 바로 뒤에, 또는 선택적인 virt-specifier(override나 final) 뒤에 등장하죠. pure-specifier는 멤버 함수 정의나 friend 선언에는 나타날 수 없어요.
struct Base
{
virtual int g();
virtual ~Base() {}
};
struct A : Base
{
// OK: 멤버 가상 함수 세 개를 선언하는데, 그중 둘은 순수 가상
virtual int f() = 0, g() override = 0, h();
// OK: 소멸자도 순수 가상이 될 수 있어요
~A() = 0;
// Error: 함수 정의에 pure-specifier 사용
virtual int b() = 0 {}
};
**추상 클래스(abstract class)**는, 최종 오버라이더(final overrider)가 순수 가상인 함수를 하나 이상 정의하거나 상속받는 클래스예요.
추상 클래스는 개념(concept)을 표현할 때 쓰여요. Shape, Animal 같은 일반적인 개념은 그 자체로 객체가 될 수 없지만, 구체 클래스 Circle, Dog 같은 것들의 기반이 되죠.
추상 클래스의 객체는 만들 수 없어요(그것을 상속한 클래스의 기반 부분 객체는 제외). 또 타입이 추상 클래스인 비정적 데이터 멤버도 선언할 수 없어요.
추상 타입은 매개변수 타입, 함수 반환 타입, 명시적 변환의 타입으로도 쓸 수 없어요. 다만 이 검사는 정의 지점과 함수 호출 지점에서 이뤄져요. 함수 선언 지점에서는 매개변수·반환 타입이 미완성(incomplete)일 수 있기 때문이에요.
추상 클래스에 대한 포인터와 참조는 선언할 수 있어요.
struct Abstract
{
virtual void f() = 0; // 순수 가상
}; // "Abstract"는 추상
struct Concrete : Abstract
{
void f() override {} // 순수 가상이 아닌 것
virtual void g(); // 순수 가상이 아닌 것
}; // "Concrete"는 비추상
struct Abstract2 : Concrete
{
void g() override = 0; // 순수 가상 오버라이더
}; // "Abstract2"는 추상
int main()
{
// Abstract a; // Error: 추상 클래스
Concrete b; // OK
Abstract& a = b; // OK: 추상 기반을 참조하는 건 가능
a.f(); // Concrete::f()로 가상 디스패치
// Abstract2 a2; // Error: 추상 클래스 (g()의 최종 오버라이더가 순수 가상)
}
순수 가상 함수의 정의는 제공할 수 있어요(그리고 순수 가상이 소멸자라면 반드시 제공해야 해요). 파생 클래스의 멤버 함수는 한정 함수 id(qualified function id)로 추상 기반의 순수 가상 함수를 호출할 수 있죠. 이 정의는 반드시 클래스 본문 바깥에서 제공해야 해요. 함수 선언 문법은 pure-specifier = 0와 함수 본문을 동시에 허용하지 않으니까요.
추상 클래스의 생성자나 소멸자에서 순수 가상 함수를 가상 호출하는 것은 정의되지 않은 동작(undefined behavior)이에요(정의가 있든 없든 마찬가지예요).
struct Abstract
{
virtual void f() = 0; // 순수 가상
virtual void g() {} // 순수 가상이 아님
~Abstract()
{
g(); // OK: Abstract::g() 호출
// f(); // undefined behavior
Abstract::f(); // OK: 비가상 호출
}
};
// 순수 가상 함수의 정의
void Abstract::f()
{
std::cout << "A::f()\n";
}
struct Concrete : Abstract
{
void f() override
{
Abstract::f(); // OK: 순수 가상 함수 호출
}
void g() override {}
~Concrete()
{
g(); // OK: Concrete::g() 호출
f(); // OK: Concrete::f() 호출
}
};
더 알아보기
virtual지정자가 가상 함수의 동작을 어떻게 정의하는지 살펴보면 이 페이지가 훨씬 잘 읽혀요.override와final지정자가 최종 오버라이더 결정에 어떻게 관여하는지도 이어서 보면 좋아요.- cppreference의 추상 클래스 원문에서 예외 사항을 더 확인할 수 있어요.