【C++】virtual 与多态

1 前言

The virtual keyword declares a virtual function or a virtual base class. —— virtual (C++) -Microsoft Docs

从微软的这篇文档来看,virtual 用于修饰

  • 方法
  • 基类

多态(Polymorphism):当用于面向对象编程的范畴时,多态性的含义是指程序能通过引用或指针的动态类型获取特定的行为的能力。

2 关键字 virtual

virtual 和多态可以说是密不可分的,

2.1 虚函数

A virtual function is a member function that you expect to be redefined in derived classes. When you refer to a derived class object using a pointer or a reference to the base class, you can call a virtual function for that object and execute the derived class’s version of the function.
Virtual functions ensure that the correct function is called for an object, regardless of the expression used to make the function call. —— Virtual Functions -Microsoft Docs

虚函数(virtual function)有几个特点

  • 是类成员方法,一般在基类中声明
  • 保证了对象调用了正确的方法

怎么理解“调用了正确的方法”?首先我们先要明白 Override(覆盖或称重写)的含义,注意不是 Overload 重载。覆盖即在派生类中重新定义基类中的方法,当然返回类型、函数名、参数列表需要完全一致。了解了以后我们来看一段代码:

class Base {
public:
    void Print() {
        cout << "Base::Print()\n";
    }
};

class Derived : public Base {
public:
    void Print() { //重写了父类中的方法
        cout << "Derived::Print()\n";
    }
};


int main() {
    Base* b = new Base();
    Derived* d = new Derived();
    Base* p1 = nullptr;
    Derived* p2 = nullptr;

    //Base指针指向Base
    p1 = b;
    p1->Print();

    //Base指针指向Derived
    p1 = d;
    p1->Print();

    //Derived指针指向base
    //p2 = b; //错误,编译器:无法从“Base *”转换为“Derived *”
    p2 = static_cast<Derived*> (b); // 上述操作,可以使用 dynamic_cast 或 static_cast 完成,不过这里并不安全
    p2->Print();

    //Derived指向Derived
    p2 = d;
    p2->Print();
}

上述程序的输出为

Base::Print() //Base指针指向Base
Base::Print() //Base指针指向Derived
Derived::Print() //static_cast转换
Derived::Print() //Derived指向Derived

刚刚的程序,我们有父类指针(基类指针,在这里即 Base* p1)指向子类,子类指针通过特殊的方法指向父类。而引入 static_castdynamic_cast 有些麻烦,我们在这里先假定子类指针是不能指向父类的。

从程序的运行结果看来,在普通的重写情况(即不加 virtual 修饰)下,当基类指针指向派生类时,并不能正确调用派生类重写了的方法,运行的时候实则调用了基类中的定义,然而这并不是我们想要的。这个时候 virtual 就发挥作用了!

2.1.1 在 Base 中声明虚函数

class Base {
public:
    virtual void Print() { //声明为虚函数
        cout << "Base::Print()\n";
    }
};

class Derived : public Base {
public:
    void Print() { //隐式声明虚函数
        cout << "Derived::Print()\n";
    }
};

class DerivedDerived : public Derived {
public:
    void Print() { //隐式声明虚函数
        cout << "DerivedDerived::Print()\n";
    }
};

继承关系:Base <= Derived <= DerivedDerived,和前面的举例一样,通过改变 Base 指针指向的实例,我们有如下的运行结果

Base::Print() //Base指针指向Base实例
Derived::Print() //Base指针指向Derived实例
DerivedDerived::Print() //Base指针指向DerivedDerived实例

没错,这就是我们想要的结果。此外,当基类声明为虚函数以后,它的派生类、派生类的派生类…即便重写的时候不写 virtual 也会被隐式声明。不过我建议还是显式写一下,这样方便阅读与维护,不过后面有更好的方法。

2.1.2 在 Derived 中声明虚函数

class Base {
public:
    void Print() {
        cout << "Base::Print()\n";
    }
};

class Derived : public Base {
public:
    virtual void Print() { //声明为虚函数
        cout << "Derived::Print()\n";
    }
};

class DerivedDerived : public Derived {
public:
    virtual void Print() { //声明为虚函数,加不加virtual都行
        cout << "DerivedDerived::Print()\n";
    }
};

考虑一下上面的情况,会出现以下的结果

Base::Print() //Base指针指向Base实例
Base::Print() //Base指针指向Derived实例
Base::Print() //Base指针指向DerivedDerived实例
Derived::Print() //Derived指针指向Derived实例
DerivedDerived::Print() //Derived指针指向DerivedDerived实例

这和我猜想的一致,在 Derived 中才声明为虚函数,并不能影响到 Base

2.1.3 范围解析操作符抑制 virtual 机制

The virtual function-call mechanism can be suppressed by explicitly qualifying the function name using the scope-resolution operator ::

我们可以通过范围解析操作符(scope-resolution operator):: 抑制 virtual 机制

class Base {
public:
    virtual void Print() { //显式声明为虚函数
        cout << "Base::Print()\n";
    }
};

class Derived : public Base {
public:
    virtual void Print() { //显式声明为虚函数
        cout << "Derived::Print()\n";
    }
};

class DerivedDerived : public Derived {
public:
    virtual void Print() { //显式声明为虚函数
        cout << "DerivedDerived::Print()\n";
    }
};

先来看子类指针的使用情况

DerivedDerived* dd = new DerivedDerived();
dd->Base::Print();
dd->Derived::Print();
dd->DerivedDerived::Print();
dd->Print();

// 输出和预想一样
// Base::Print()
// Derived::Print()
// DerivedDerived::Print()
// DerivedDerived::Print()

再看看其他的使用情况

DerivedDerived* dd = new DerivedDerived();
Base* b = dd;
b->Base::Print();
//b->Derived::Print() //编译错误,因为b时是base指针
dynamic_cast<Derived*>(p1)->Derived::Print(); //可通过 dynamic_cast 实现

// 输出
// Base::Print()
// Derived::Print()

一般来说,使用范围解析运算符(作用域运算符)回避虚函数的机制场合——比如说你想在派生类中调用基类的虚方法。但要注意不要忘记加作用域否则可能造成递归。

2.1.4 添加 final 和 override 说明符

前面提到最好显式地使用 virtual 来方便阅读和维护,不过在 C++11 中引入了 overridefinal 说明符,可以更加好的达成目的。

class Base {
public:
    virtual void Print() { //声明为虚函数
        cout << "Base::Print()\n";
    }
    void f1();
    virtual void f2() final;
};

class Derived : public Base {
public:
    void Print() override { //override说明
        cout << "Derived::Print()\n";
    }
    // void f1() override; //错误,f1 不是虚函数
    // void f2() override; //错误,f2 被修饰为 final,无法被覆盖
};

为什么这样更好呢?还记得 overload 重载吗?如果我们在派生类重写方法时,不小心搞错了参数列表,如此一来实际上我们完成了一个函数重载,而不是重写。此时我们写的方法并没能达成覆盖掉基类方法的目的,这样编译能通过,但显然不是我们想要的,要想调试找到这种错误很困难。但是加上了 override 就不一样了,如果子类在重写时,没能在父类中找到被覆盖的方法,编译会报错!而且如果不是虚函数使用 override 说明符也会报错。

final 更不必多说,和它的名字一样添加之后,继承它的派生类的如果企图覆盖该方法,都会引发错误。

2.1.5 默认实参的问题

如果某次函数使用了默认实参,则该实参值由本次调用的静态类型决定。所以这里建议两点:

  • 不使用默认实参
  • 或使用时,基类和派生类定义的默认实参最好一致

注:最后再讲一点,析构函数最好声明为虚函数,而构造函数不能为虚函数。至于为什么析构函数要被声明为虚函数?因为想要对象执行正确的析构函数版本,结合前面的例子仔细品一品;那构造函数不能为虚函数?仔细想想,创建对象的时候必须得指定对象类型吧。

2.2 纯虚函数与抽象类

声明为纯虚函数(pure virtual function)只需要在虚函数的最后加上 =0,举个例子:

class Base {
public:
    virtual void Print() = 0; //声明为纯虚函数
};

需要注意以下几点:

  • 声明为纯虚函数说明这个函数没有定义,需要由继承它的类去完成它的定义
  • 当类中声明了一个纯虚函数后,该类就变成了抽象类
  • 如果不在继承类中实现该函数,则继承类仍为抽象类(abstract class)
  • 抽象类是不能实例化为对象的,但可以有构造函数

2.3 虚基类与虚继承

首先引入多重继承(multiple inheritance)的概念:指从多个直接基类中产生派生类的能力。多重继承的派生类继承了所有父类的属性,但多个基类交织可能会产生很多问题。我们来看下面的继承关系,istream 与 ostream 继承了 base_ios,iostream 由 istream 与 ostream 直接继承而来。这样一来,iostream 继承了 base_ios 两次,这样岂不是有了 base_ios 的两份拷贝?但事实上在这个例子里面,我们并不想这样。(因为在 base_ios 里保存着流的缓冲内容,而 iostream 的读写操作肯定想在同一个缓冲区进行读写操作)

【C++】virtual 与多态_第1张图片

虚继承(virtual inheritance)主要是为了解决上面提到的多重继承的问题。虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类(virtual base class)。接下来我们举个例子,有这样一个继承关系:

【C++】virtual 与多态_第2张图片

使用虚基类的方式如下

// public 和 virtual 顺序随意,我习惯 virtual 在前
class Bear : virtual public ZooAnimal { /* ... */ };
class Raccoon : public virtual ZooAnimal { /* ... */ };
class Endangered { /* ... */};
class Panda : public Bear, public Raccoon, public Endangered { /* ... */ };

简单的解释一下,Panda 通过 Raccoon 和 Bear 继承了 ZooAnimal,因为 Raccoon 和 Bear 继承 ZooAnimal 的方式都是虚继承,所以在 Panda 中只有一个 ZooAnimal 基类部分。另外,要注意以下几点:

  • 如果某个类指定了虚基类,则该类的派生类仍按常规方式进行,就如前面的 panda 所示
  • 不论是基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作

2.3.1 虚基类成员的可见性

以前面的继承关系为例,ZooAnimal 中定义了一个名为 x 的成员,如果我们通过 Panda 的对象使用 x,有三种可能性

  1. Bear 和 Raccoon 都没有 x 的定义,则 x 将被解析为 ZooAnimal 的成员,此时不存在二义性,一个 Panda 对象只含有 x 的一个实例
  2. 如果 Bear 或 Raccoon 中的某一个覆盖了 x ,此时同样没有二义性,派生类的 x 比共享虚基类的优先级高
  3. 如果 Bear 或 Raccoon 中都覆盖了 x,则直接访问 x 将产生二义性问题

解决问题的上述问题的最好办法就是在派生类中为成员自定义新的实例

2.3.2 构造函数与虚继承

在虚继承中,虚基类是由最底层的派生类初始化的,这里和普通的构造规则有所不同。如果我们以普通规则处理初始化任务,虚基类将会在多条继承路径上被重复初始化,结合前面的例子理解一下,我们对 Zoo

class ZooAnimal {
public: 
    int x;
    ZooAnimal(int x = 4) : x(x) { //自定义了默认构造函数
        cout << "ZooAnimal Constructor\n";
    }
};

如果应用普通规则,在 Panda 中我们通过使用两个直接基类的构造函数初始化,Raccoon 和 Bear 都会试图初始化 Panda 的 ZooAnimal 部分。(那可出问题了)我们应该怎么办呢?

继承体系的每个类都可能在某个时刻成为“最低层的派生类”。只要我们能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。 ——《C++ Primer》

怎么理解这句话?例如在我们的继承体系中,当创建一个 Bear(或 Raccoon)的对象时,它已经位于派生类的最低层,因此 Bear(或 Raccoon)的构造函数将直接初始化其 ZooAnimal 基类部分。

// 一般我们会在在初始化列表进行成员初始化
Bear::Bear(int x = 1) : ZooAnimal(x) { 
    cout << "Bear Constructor\n";
}

Raccoon::Raccoon(int x = 2) : ZooAnimal(x) {
    cout << "Raccoon Constructor\n";
}

// 但如果不显式的在初始化列表里面初始化,将会调用成员的默认构造函数(在这里为 ZooAnimal)...
// 且如果没有默认构造函数则会报错(ZooAnimal),见下面

// ZooAnimal::ZooAnimal() = delete; //这里声明没有默认构造函数
// Bear::Bear() {} //错误,ZooAnimal 没有默认构造函数

当我们创建一个 Panda 对象时,Panda 位于派生的最低层并由它负责初始化共享的 ZooAnimal 基类部分。即使 ZooAnimal 不是 Panda 的直接基类,Panda 的构造函数也可以直接初始化 ZooAnimal。见下:


Endangered::Endangered() {
    cout << "Endangered Constructor\n";
}

class Panda : public Bear, public Raccoon, public Endangered {
public:
    Panda(int x = 3) : ZooAnimal(x), Raccoon(x), Bear(x) {
        cout << "Panda Constructor\n";
    }
};

int main(){
    Panda p;
    cout << p.x;
}

// 输出结果
// ZooAnimal Constructor
// Raccoon Constructor
// Bear Constructor
// Endangered Constructor
// Panda Constructor
// 3

对照上面的输出,讲一下构造顺序:

  1. 使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分
  2. 按照派生列表中出现的次序依次对其初始化(例如声明 Panda 类时,派生列表是先写的 Bear 再写的 Raccoon)

注:建议自己写一写代码试一试

其他

待补充…

参考

[] 虚函数 -《C++ Primer(第5版)中文版》 p536

[] Virtual Functions -Microsoft Docs https://docs.microsoft.com/zh-cn/cpp/cpp/virtual-functions?view=vs-2019

[] C++虚继承和虚基类详解 http://c.biancheng.net/view/2280.html

[] 多重继承与虚继承 -《C++ Primer(第5版)中文版》 p710

你可能感兴趣的:(C++)