Namespaces
Variants

virtual function specifier

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Virtual function
override specifier (C++11)
final specifier (C++11)
Special member functions
Templates
Miscellaneous

비정적 멤버 함수 virtual 이며 동적 디스패치를 지원하도록 지정합니다. 이는 비정적 멤버 함수의 최초 선언(즉, 클래스 정의 내에서 선언될 때)의 decl-specifier-seq 에서만 나타날 수 있습니다.

목차

설명

가상 함수는 파생 클래스에서 동작을 재정의할 수 있는 멤버 함수입니다. 비가상 함수와 달리, 클래스의 실제 타입에 대한 컴파일 타임 정보가 없는 경우에도 재정의된 동작이 유지됩니다. 즉, 기본 클래스에 대한 포인터나 참조를 사용하여 파생 클래스를 다룰 때, 재정의된 가상 함수에 대한 호출은 파생 클래스에서 정의된 동작을 호출합니다. 이러한 함수 호출은 가상 함수 호출 또는 가상 호출 이라고 합니다. 함수가 한정된 이름 검색 을 사용하여 선택된 경우(즉, 함수 이름이 범위 지정 연산자 :: 의 오른쪽에 나타나는 경우) 가상 함수 호출은 억제됩니다.

#include <iostream>
struct Base
{
    virtual void f()
    {
        std::cout << "base\n";
    }
};
struct Derived : Base
{
    void f() override // 'override'는 선택 사항입니다
    {
        std::cout << "derived\n";
    }
};
int main()
{
    Base b;
    Derived d;
    // 참조를 통한 가상 함수 호출
    Base& br = b; // br의 타입은 Base&
    Base& dr = d; // dr의 타입 역시 Base&
    br.f(); // "base" 출력
    dr.f(); // "derived" 출력
    // 포인터를 통한 가상 함수 호출
    Base* bp = &b; // bp의 타입은 Base*
    Base* dp = &d; // dp의 타입 역시 Base*
    bp->f(); // "base" 출력
    dp->f(); // "derived" 출력
    // 비가상 함수 호출
    br.Base::f(); // "base" 출력
    dr.Base::f(); // "base" 출력
}

상세히

어떤 멤버 함수 vf 가 클래스 Base 에서 virtual 로 선언되고, Base 로부터 직접적 또는 간접적으로 파생된 어떤 클래스 Derived 가 동일한 멤버 함수 선언을 가지는 경우

  • 이름
  • 매개변수 타입 목록 (반환 타입은 제외)
  • cv 한정자
  • 참조 한정자

그러면 이 클래스 Derived 내의 함수 또한 virtual (선언부에 virtual 키워드 사용 여부와 관계없이)이며 Base::vf를 재정의(overrides) 합니다 (선언부에 override 지정자 사용 여부와 관계없이).

Base::vf 는 재정의하기 위해 접근 가능하거나 보일 필요가 없습니다. ( Base::vf 는 private로 선언될 수 있으며, 또는 Base 가 private 상속을 사용하여 상속될 수 있습니다. Base 를 상속하는 Derived 의 기본 클래스에서 동일한 이름을 가진 모든 멤버들은 이름 검색 중에 Base::vf 를 숨기더라도 재정의 결정에 영향을 미치지 않습니다.)

class B
{
    virtual void do_f(); // private 멤버
public:
    void f() { do_f(); } // public 인터페이스
};
struct D : public B
{
    void do_f() override; // B::do_f를 재정의
};
int main()
{
    D d;
    B* bp = &d;
    bp->f(); // 내부적으로 D::do_f() 호출
}

모든 가상 함수에 대해 최종 재정의자(final overrider) 가 존재하며, 가상 함수 호출이 발생할 때 이 함수가 실행됩니다. 기본 클래스 Base 의 가상 멤버 함수 vf 는 파생 클래스가 vf 를 재정의하는 다른 함수를 선언하거나 (다중 상속을 통해) 상속받지 않는 한 최종 재정의자입니다.

struct A { virtual void f(); };     // A::f는 가상 함수입니다
struct B : A { void f(); };         // B::f는 B에서 A::f를 재정의합니다
struct C : virtual B { void f(); }; // C::f는 C에서 A::f를 재정의합니다
struct D : virtual B {}; // D는 재정의자를 도입하지 않으며, B::f는 D에서 final입니다
struct E : C, D          // E는 재정의자를 도입하지 않으며, C::f는 E에서 final입니다
{
    using A::f; // 함수 선언이 아닌, 단지 A::f를 조회에 보이게 만듭니다
};
int main()
{
    E e;
    e.f();    // 가상 호출은 e의 최종 재정의자인 C::f를 호출합니다
    e.E::f(); // 비가상 호출은 E에서 보이는 A::f를 호출합니다
}

함수에 둘 이상의 최종 재정의자가 있는 경우 프로그램의 형식이 잘못되었습니다:

struct A
{
    virtual void f();
};
struct VB1 : virtual A
{
    void f(); // A::f를 재정의함
};
struct VB2 : virtual A
{
    void f(); // A::f를 재정의함
};
// struct Error : VB1, VB2
// {
//     // 오류: Error에서 A::f에 대한 두 개의 최종 재정의자가 존재함
// };
struct Okay : VB1, VB2
{
    void f(); // OK: 이것이 A::f에 대한 최종 재정의자임
};
struct VB1a : virtual A {}; // 재정의자를 선언하지 않음
struct Da : VB1a, VB2
{
    // Da에서 A::f의 최종 재정의자는 VB2::f임
};

동일한 이름을 가지지만 매개변수 목록이 다른 함수는 동일한 이름의 기본 함수를 재정의(override)하지 않고 숨깁니다(hides) : 비한정 이름 조회(unqualified name lookup) 가 파생 클래스의 범위를 검사할 때, 조회는 해당 선언을 찾고 기본 클래스를 검사하지 않습니다.

struct B
{
    virtual void f();
};
struct D : B
{
    void f(int); // D::f가 B::f를 숨김 (잘못된 매개변수 목록)
};
struct D2 : D
{
    void f(); // D2::f가 B::f를 재정의 (보이지 않는다는 것은 중요하지 않음)
};
int main()
{
    B b;
    B& b_as_b = b;
    D d;
    B& d_as_b = d;
    D& d_as_d = d;
    D2 d2;
    B& d2_as_b = d2;
    D& d2_as_d = d2;
    b_as_b.f();  // B::f() 호출
    d_as_b.f();  // B::f() 호출
    d2_as_b.f(); // D2::f() 호출
    d_as_d.f();  // 오류: D에서 조회 시 f(int)만 찾음
    d2_as_d.f(); // 오류: D에서 조회 시 f(int)만 찾음
}

함수가 override 지정자로 선언되었지만 가상 함수를 재정의하지 않으면 프로그램은 형식이 잘못됨:

struct B
{
    virtual void f(int);
};
struct D : B
{
    virtual void f(int) override;  // OK, D::f(int) overrides B::f(int)
    virtual void f(long) override; // Error: f(long) does not override B::f(int)
};

함수가 final 지정자로 선언되고 다른 함수가 이를 재정의하려고 시도하면 프로그램은 형식이 잘못됨:

struct B
{
    virtual void f() const final;
};
struct D : B
{
    void f() const; // Error: D::f attempts to override final B::f
};
(C++11부터)

비멤버 함수와 정적 멤버 함수는 가상 함수로 선언할 수 없습니다.

함수 템플릿은 virtual 로 선언될 수 없습니다. 이는 템플릿 자체인 함수에만 적용됩니다 - 클래스 템플릿의 일반 멤버 함수는 virtual로 선언될 수 있습니다.

가상 함수(가상으로 선언되었거나 하나를 재정의하는 경우)는 어떠한 연관된 제약 조건도 가질 수 없습니다.

struct A
{
    virtual void f() requires true; // Error: constrained virtual function
};

consteval 가상 함수는 비- consteval 가상 함수에 의해 재정의되거나 재정의해서는 안 됩니다.

(since C++20)

기본 인수 는 가상 함수에 대해 컴파일 타임에 치환됩니다.

공변 반환 타입

함수 Derived::f 가 함수 Base::f 를 재정의하는 경우, 두 함수의 반환 타입은 동일하거나 공변적(covariant) 이어야 합니다. 두 타입이 공변적이기 위해서는 다음의 모든 요구사항을 충족해야 합니다:

  • 두 타입 모두 클래스에 대한 포인터 또는 참조(좌측값 또는 우측값)여야 합니다. 다중 레벨 포인터 또는 참조는 허용되지 않습니다.
  • Base::f() 의 반환 타입에서 참조/포인팅되는 클래스는 Derived::f() 의 반환 타입에서 참조/포인팅되는 클래스의 명확하고 접근 가능한 직접 또는 간접 기본 클래스여야 합니다.
  • Derived::f() 의 반환 타입은 Base::f() 의 반환 타입보다 동등하거나 더 적은 cv-qualified 이어야 합니다.

Derived::f 의 반환 타입에 있는 클래스는 Derived 자신이거나, Derived::f 의 선언 지점에서 완전한 타입 이어야 합니다.

가상 함수 호출이 발생할 때, 최종 오버라이더가 반환하는 타입은 호출된 오버라이드된 함수의 반환 타입으로 암시적으로 변환 됩니다:

class B {};
struct Base
{
    virtual void vf1();
    virtual void vf2();
    virtual void vf3();
    virtual B* vf4();
    virtual B* vf5();
};
class D : private B
{
    friend struct Derived; // Derived 내에서 B는 D의 접근 가능한 기본 클래스입니다
};
class A; // 전방 선언된 클래스는 불완전한 타입입니다
struct Derived : public Base
{
    void vf1();    // 가상 함수, Base::vf1()을 재정의합니다
    void vf2(int); // 비가상 함수, Base::vf2()를 숨깁니다
//  char vf3();    // 오류: Base::vf3을 재정의하지만 다른
                   // 그리고 공변 반환 타입이 아닙니다
    D* vf4();      // Base::vf4()을 재정의하고 공변 반환 타입을 가집니다
//  A* vf5();      // 오류: A는 불완전한 타입입니다
};
int main()
{
    Derived d;
    Base& br = d;
    Derived& dr = d;
    br.vf1(); // Derived::vf1()을 호출합니다
    br.vf2(); // Base::vf2()를 호출합니다
//  dr.vf2(); // 오류: vf2(int)가 vf2()를 숨깁니다
    B* p = br.vf4(); // Derived::vf4()을 호출하고 결과를 B*로 변환합니다
    D* q = dr.vf4(); // Derived::vf4()을 호출하고 결과를 B*로 변환하지 않습니다
}

가상 소멸자

소멸자는 상속되지 않지만, 기반 클래스가 소멸자를 virtual 로 선언하면 파생 소멸자는 항상 이를 재정의합니다. 이를 통해 기반 클래스에 대한 포인터를 통해 다형적 타입의 동적 할당 객체를 삭제할 수 있습니다.

class Base
{
public:
    virtual ~Base() { /* Base의 자원을 해제합니다 */ }
};
class Derived : public Base
{
    ~Derived() { /* Derived의 자원을 해제합니다 */ }
};
int main()
{
    Base* b = new Derived;
    delete b; // Base::~Base()에 대한 가상 함수 호출을 수행합니다
              // 가상 함수이므로 Derived::~Derived()를 호출하여 파생 클래스의
              // 자원을 해제할 수 있으며, 이후 일반적인 소멸 순서에 따라
              // Base::~Base()를 호출합니다
}

게다가, 기본 클래스의 소멸자가 가상이 아닌 경우, 파생 클래스 객체를 기본 클래스 포인터를 통해 삭제하는 것은 정의되지 않은 동작 입니다. 파생 소멸자가 호출되지 않을 경우 누출될 리소스의 존재 여부와 관계없이 , 선택된 할당 해제 함수가 파괴적 operator delete 가 아닌 한 (C++20부터) .

유용한 지침으로, 삭제 표현식이 관련될 때마다 모든 기본 클래스의 소멸자는 public 및 virtual이거나 protected 및 non-virtual이어야 합니다 , 예를 들어 암시적으로 사용될 때 std::unique_ptr (C++11부터) .

생성 및 소멸 과정 중

가상 함수가 생성자나 소멸자로부터 직접 또는 간접적으로 호출될 때 (클래스의 비정적 데이터 멤버 생성 또는 소멸 과정 중, 예를 들어 멤버 initializer list 에서), 그리고 호출이 적용되는 객체가 생성 중이거나 소멸 중인 객체인 경우, 호출되는 함수는 생성자나 소멸자의 클래스에서 final overrider이며 더 파생된 클래스에서 이를 오버라이딩하는 함수가 아닙니다. 다시 말해, 생성 또는 소멸 과정 중에는 더 파생된 클래스들은 존재하지 않습니다.

여러 분기를 가진 복잡한 클래스를 구성할 때, 한 분기에 속하는 생성자 내에서는 다형성이 해당 클래스와 그 기반 클래스로 제한됩니다: 만약 이 하위 계층 구조 밖에 있는 기반 하위 객체에 대한 포인터나 참조를 획득하고, 가상 함수 호출을 시도하는 경우(예: 명시적 멤버 접근 사용), 그 동작은 정의되지 않습니다:

struct V
{
    virtual void f();
    virtual void g();
};
struct A : virtual V
{
    virtual void f(); // A::f는 A에서 V::f의 최종 오버라이더입니다
};
struct B : virtual V
{
    virtual void g(); // B::g는 B에서 V::g의 최종 오버라이더입니다
    B(V*, A*);
};
struct D : A, B
{
    virtual void f(); // D::f는 D에서 V::f의 최종 오버라이더입니다
    virtual void g(); // D::g는 D에서 V::g의 최종 오버라이더입니다
    // 참고: A는 B보다 먼저 초기화됩니다
    D() : B((A*) this, this) {}
};
// D의 생성자에서 호출되는 B의 생성자
B::B(V* v, A* a)
{
    f(); // V::f에 대한 가상 호출 (D가 최종 오버라이더를 가지고 있지만, D는 아직 존재하지 않음)
    g(); // B::g에 대한 가상 호출, 이는 B에서의 최종 오버라이더입니다
    v->g(); // v의 타입 V는 B의 기반 클래스이며, 가상 호출은 이전과 같이 B::g를 호출합니다
    a->f(); // a의 타입 A는 B의 기반 클래스가 아닙니다. 이는 계층 구조의 다른 분기에 속합니다.
            // 해당 분기를 통한 가상 호출 시도는 이 경우 A가 이미 완전히 생성되었음에도
            // (D의 기반 클래스 목록에서 B보다 앞에 나타나므로 B보다 먼저 생성되었음)
            // 미정의 동작을 유발합니다. 실제로는 B의 생성 중에 활성화된 B의 가상 멤버 함수 테이블을
            // 사용하여 A::f에 대한 가상 호출이 시도될 것입니다
}

키워드

virtual

결함 보고서

다음의 동작 변경 결함 보고서들은 이전에 발표된 C++ 표준에 소급 적용되었습니다.

DR 적용 대상 게시된 동작 올바른 동작
CWG 258 C++98 파생 클래스의 비-const 멤버 함수가 기본 클래스의 const 가상 멤버 함수 때문에
가상 함수가 될 수 있었음
가상성은 cv-한정자도
동일해야 함
CWG 477 C++98 friend 선언에 virtual 지정자를 포함할 수 있었음 허용되지 않음
CWG 1516 C++98 "virtual function call"과 "virtual call" 용어의 정의가
제공되지 않았음
제공됨

참고 항목

파생 클래스와 상속 모드
override 지정자 (C++11) 메서드가 다른 메서드를 재정의함을 명시적으로 선언
final 지정자 (C++11) 메서드가 재정의될 수 없거나 클래스가 파생될 수 없음을 선언