这篇文章上次修改于 896 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

总结了C++中 virtual 关键字的使用场景或注意事项,注意文中并不包含部分众所周知常见知识点。

虚函数的默认实参

实参值的默认值由调用时的静态类型决定。即如果通过基类的引用或指针调用虚函数,则使用基类中的默认实参,此时,传入派生类函数的将是基函数定义的默认实参值(虽然实际运行的时派生类的实例)。

提示:如果虚函数使用了默认实参,那么基类派生类中定义的默认实参最好保持一致。

虚析构函数

使用具有非虚拟析构函数的基类类型的指针删除派生类对象会导致未定义的行为。要纠正这种情况,应使用虚拟析构函数定义基类。

一个准则是如果类中有虚函数,那么该类就应实现虚析构函数(即使它什么都不做)。

析构函数的虚属性同其他函数一样也会被派生类集成,故派生类的析构函数不必须定义为虚函数。但为保证程序良好可读性,派生类最好也添加上virtual关键字。

内联虚函数

每当使用基类引用或指针调用虚函数时,它都不能内联,因为调用是在运行时解析的;但是当使用该类的对象(没有引用或指针)调用时,是可以内联,因为编译器知道确切的编译时对象的类。

// CPP program to demonstrate that
// virtual functions can be inlined
#include <iostream>
using namespace std;

class Base {
public:
    virtual void who() { cout << "I am Base\n"; }
};
class Derived : public Base {
public:
    void who() { cout << "I am Derived\n"; }
};

int main()
{
    // Part 1
    Base b;
    b.who();

    // Part 2
    Base* ptr = new Derived();
    ptr->who();

    return 0;
}
  1. Part 1 中,通过类的对象调用了虚函数 who(),它将在编译时解析,因此可以内联。
  2. Part 2 中,虚函数是通过基类型指针调用的,因此不能内联。

私有虚函数

虚函数可以是私有的,因为 C++ 具有访问控制,但没有可见性控制。在派生类中可以覆盖虚函数并修改其可见性,但在任何情况下都只能在基类中调用。

有如下代码:

// C++ program to demonstrate how a
// virtual function can be private
#include <iostream>
 
class base {
public:
    // default base constructor
    base() { std::cout << "base class constructor\n"; }
 
    // virtual base destructor
    // always use virtual base
    // destructors when you know you
    // will inherit this class
    virtual ~base()
    {
        std::cout << "base class destructor\n";
    }
    // public method in base class
    void show()
    {
        std::cout << "show() called on base class\n";
    }
 
    // public virtual function in base class,
    // contents of this function are printed when called
    // with base class object when called with base class
    // pointer contents of derived class are printed on
    // screen
    virtual void print()
    {
        std::cout << "print() called on base class\n";
    }
};
 
class derived : public base {
public:
    // default derived constructor
    derived()
        : base()
    {
        std::cout << "derived class constructor\n";
    }
    // virtual derived destructor
    // always use virtual destructors
    // when inheriting from a
    // base class
    virtual ~derived()
    {
        std::cout << "derived class destructor\n";
    }
 
private:
    // private virtual function in derived class overwrites
    // base class virtual method contents of this function
    // are printed when called with base class pointer or
    // when called with derived class object
    virtual void print()
    {
        std::cout << "print() called on derived class\n";
    }
};
 
int main()
{
    std::cout << "printing with base class pointer\n";
 
    // construct base class pointer with derived class
    // memory
    base* b_ptr = new derived();
 
    // call base class show()
    b_ptr->show();
 
    // call virtual print in base class but it is overridden
    // in derived class also note that print() is private
    // member in derived class, still the contents of
    // derived class are printed this code works because
    // base class defines a public interface and drived
    // class overrides it in its implementation
    b_ptr->print();
 
    delete b_ptr;
}

上述代码可以正常编译。运行输出如下:

printing with base class pointer
base class constructor
derived class constructor
show() called on base class
print() called on derived class
derived class destructor
base class destructor

虽然在派生类中 print() 函数被覆盖,并设置可见性为 priave,但使用基类指针仍可以实现对派生类 print() 的调用。

虚继承

虚继承 可以解决多继承时可能产生菱形集成,遇到的基类成员使用出现的 二义性(ambiguous) 问题,虚继承可以使多个父类共享相同的祖父类成员。
一般来说,C++不能直接调用祖父的构造函数,祖父类构造函数必须通过父类调用,但虚继承时是允许的。

#include<iostream>
using namespace std;

class A
{
  int x;
public:
  A(int i) { x = i; cout << "A() " << x << endl; }
  void print() { cout << x << endl; }
};

class B: virtual public A
{
public:
  B():A(10) { cout << "B() "; print(); }
};

class C: virtual public A
{
public:
  C():A(20) { cout << "C() "; print(); }
};

class D: public B, public C {
public:
  D():A(30) { cout << "D() "; print(); }
};

int main()
{
    D d;
    d.print();
    return 0;
}

输出如下:

A() 30
B() 30
C() 30
D() 30
30

祖父基类 A 仅实例化一次,成员 x 值为30。
构造顺序:编译器按照直接基类的声明顺序依次检查,以确定其中是否包含虚基类。如果存在虚基类,则首先构造虚基类,然后再按声明顺序逐一构造其他非虚基类。

参考:

  1. GeeksforGeeks
  2. C++ Primer