virtual 函数指定符

来自cppreference.com
< cpp‎ | language

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 )并覆写 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 为 virtual
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; // 错误:受制约的虚函数
};
(C++20 起)

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

[编辑] 协变返回类型

若函数 Derived::f 覆写 Base::f ,则其返回类型必须相同或为协变。若满足所有下列要求,则二个类型为协变:

  • 二个类型均为到类的指针或引用(左值或右值)。不允许多级指针或引用。
  • 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 的构造中活跃
}

[编辑] 参阅