virtual 函数说明符

来自cppreference.com
< cpp‎ | language

virtual 说明符指定非静态成员函数函数并支持动态调用派发。它只能在非静态成员函数的首个声明(即当它于类定义中声明时)的 声明说明符序列 中出现。

解释

虚函数是可在派生类中覆盖其行为的成员函数。与非虚函数相反,即使没有关于该类实际类型的编译时信息,仍然保留被覆盖的行为。当使用到基类的指针或引用来处理派生类时,对被覆盖的虚函数的调用,将会调用定义于派生类中的行为。当使用有限定名字查找(即函数名出现在作用域解析运算符 :: 的右侧)时,此行为被抑制。

#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)并覆盖 Base::vf(无论其声明中是否使用单词 override)。

要覆盖的 Base::vf 不需要可见(可声明为 private,或用私有继承继承)。

class B {
    virtual void do_f(); // 私有成员
 public:
    void f() { do_f(); } // 公开接口
};
struct D : public B {
    void do_f() override; // 覆盖 B::do_f
};
 
int main()
{
    D d;
    B* bp = &d;
    bp->f(); // 内部调用 D::do_f();
}

每个虚函数都有其最终覆盖函数,它是进行虚函数调用时所执行的函数。基类 Base 的虚成员函数 vf 是最终覆盖函数,除非派生类声明或(通过多重继承)继承了覆盖 vf 的另一个函数。

struct A { virtual void f(); };     // A::f 是虚函数
struct B : A { void f(); };         // B::f 覆盖 A::f in B
struct C : virtual B { void f(); }; // C::f 覆盖 A::f in C
struct D : virtual B {}; // D 不引入覆盖函数,B::f 在 D 中为最终
struct E : C, D  {       // E 不引入覆盖函数,C::f 在 E 中为最终
    using A::f; // 非函数声明,仅令 A::f 能为查找所见
};
int main() {
   E e;
   e.f();    // 虚调用调用 C::f,e 中的最终覆盖函数
   e.E::f(); // 非虚调用调用 A::f,它在 E 中可见
}

若一个函数拥有多于一个最终覆盖函数,则程序非良构:

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 {
//     // 错误:A::f 在 Error 中拥有两个最终覆盖函数
// };
struct Okay : VB1, VB2 {
    void f(); // OK:这是 A::f 的最终覆盖函数
};
struct VB1a : virtual A {}; // 不声明覆盖函数
struct Da : VB1a, VB2 {
    // Da 中,A::f 的最终覆盖函数是 VB2::f
};

具有相同名字但不同形参列表的函数并不覆盖同名的基类函数,但会隐藏它:在无限定名字查找检查派生类的作用域时,查找找到该声明而不再检查基类。

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) 覆盖 B::f(int)
    virtual void f(long) override; // 错误:f(long) 不覆盖 B::f(int)
};

若函数以说明符 final 声明,而另一函数试图覆盖之,则程序非良构:

struct B {
    virtual void f() const final;
};
struct D : B {
    void f() const; // 错误:D::f 试图覆盖 final B::f
};
(C++11 起)

非成员函数和静态成员函数不能为虚函数。

函数模板不能被声明为 virtual。这只适用于自身是模板的函数——类模板的常规成员函数可被声明为虚函数。

虚函数(无论是声明为 virtual 者还是覆盖函数)不能有任何关联制约

struct A {
    virtual void f() requires true; // 错误:受制约的虚函数
};

consteval 虚函数不能覆盖非 consteval 虚函数或被它覆盖。

(C++20 起)

在编译时替换虚函数的默认实参

协变返回类型

若函数 Derived::f 覆盖 Base::f,则其返回类型必须要么相同要么为协变(covariant)。当满足所有下列要求时,两个类型为协变:

  • 两个类型均为到类的指针或引用(左值或右值)。不允许多级指针或引用。
  • Base::f() 的返回类型中被引用/指向的类,必须是 Derived::f() 的返回类型中被引用/指向的类的无歧义且可访问的直接或间接基类。
  • Derived::f() 的返回类型必须有相对于 Base::f() 的返回类型的相等或较少的 cv 限定

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,则派生的析构函数始终覆盖它。这使得可以通过指向基类的指针 delete 动态分配的多态类型对象

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()
}

此外,若类是多态的(声明或继承了至少一个虚函数),且其析构函数非虚,则删除它是未定义行为,无论不调用派生的析构函数时是否会导致资源泄漏。

一条有用的方针是,任何基类的析构函数必须为公开且虚,或受保护且非虚

在构造和析构期间

当从构造函数或从析构函数中直接或间接调用虚函数(包括在类的非静态数据成员的构造或析构期间,例如在成员初始化器列表中),且对其实施调用的对象是正在构造或析构中的对象时,所调用的函数是构造函数或析构函数的类中的最终覆盖函数,而非进一步的派生类中的覆盖函数。 换言之,在构造和析构期间,进一步的派生类并不存在。

当构建具有多个分支的复杂类时,在属于一个分支的构造函数内,多态被限制到该类及其基类:若它获得了指向这个子层级之外的某个基类子对象的指针或引用,且试图进行虚函数调用(例如通过显式成员访问),则行为未定义:

struct V {
    virtual void f();
    virtual void g();
};
 
struct A : virtual V {
    virtual void f(); // A::f 是 V::f 在 A 中的最终覆盖函数
};
struct B : virtual V {
    virtual void g(); // B::g 是 V::g 在 B 中的最终覆盖函数
    B(V*, A*);
};
struct D : A, B {
    virtual void f(); // D::f 是 V::f 在 D 中的最终覆盖函数
    virtual void g(); // D::g 是 V::g 在 D 中的最终覆盖函数
 
    // 注意:A 在 B 之前初始化
    D() : B((A*)this, this) 
    {
    }
};
 
// B 的构造函数,从 D 的构造函数调用 
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 已完成构造
            // (它在 B 之前构造,因为它在 D 的基类列表中先于 B 出现)
            // 实践中,对 A::f 的虚调用会试图使用 B 的虚成员函数表,
            // 因为它在 B 的构造期间是活跃的
}

参阅