C++虚方法


​ 关键字 virtual 经常定义在C++类中的方法上. 这个关键字可以声明这个方法是一个虚方法.

​ 虚方法的意义在于, 让程序根据实际的对象类型选择实现的函数.

​ 众所周知, 在类继承的过程中, 子类可以继承基类的方法. 当子类重新定义了一个与基类声明完全一致的方法时, 基类可以重写这个方法. 在通常使用对象的情况下, 对于两个行为不同的方法, 程序会按照使用的对象类型来决定使用哪个版本的方法.

​ 但作为一个C++程序, 通常不会以普通传值的方式传递对象,而是传递一个指针或者引用(尤其是引用)。

​ 于此同时,在编码过程中难以预料地会出现用父类变量(包括指针变量和引用变量)来传递一个子类对象的情况。这个时候对于一个子类对象,按照程序预先设定的方式,会调用父类相应的同名方法。如果子类对这个方法有特殊要求,则会被忽略掉。这是我们不想看到的。

virtual关键字就是为了解决这个问题。这个关键字实现了这样一件事:当这个方法被父类声明为virtual时,在针对一个指针或者引用调用这个方法时会自动根据这个引用或者指针所指向的对象类型来决定使用哪个方法,而非仅仅通过这个引用或者指针变量本身的类型来决定使用哪个版本的方法。

来看个例子,这个例子来自C++ Primer Plus

/***************************************
 * File: brass.h
 * Bank account classes
***************************************/

#ifndef BRASS_H_
#define BRASS_H_

#include <string>

// Brass Account Class (Base class)

class Brass
{
private:
    std::string fullName;
    long acctNum;
    double balance;
public:
    Brass(const std::string & s = NUllbody", long an = -1, double bal = 0.0);
    void Deposit(double amt);
    virtual Withdraw(double amt);
    double Balance() const;
    virtual void ViewAcct() const;
    virtual ~Brass() {}//注意,这里的析构函数是虚函数
}

这是一个基类, 如你所见, 这个基类的Balance()方法和withdraw()声明为虚方法.

于此同时, 有一个名为BalencePlus的类继承于这个基类

//Brass Plus Account Class
class BrassPlus : public Brass
{
private:
    double maxLoan;
    double rate;
    double owesBank;
public:
    BrassPlus(const std::string & s = "Nullbody", long an = -1),
    ...;
    virtual void ViewAcct() const;
    virtual void Withdraw(double amt);
    ...; //一些基类没有的方法
    //注意,这个子类没有显式声明析构函数
}

为基类声明一个虚的析构函数可以确保在子类被释放时以正确的方式析构,避免出现内存泄漏。应始终为基类声明虚的析构函数

动态联编和静态联编

联编:决定使用哪个版本的函数(Overload 的还是Override的还是原来的)

静态联编(static binding) :在编译时确定(针对Overload的函数,可以在编译时根据函数参数确定)

动态联编:(Dynamic binding):在运行时确定,针对Override之后的virutal函数

如何进行动态联编呢?这是基于C++指针和引用类型的兼容性进行的。

由于类的继承表现的是一种is-a关系,所以这种关系决定了父类指针或引用可以指向子类对象的地址。这是向上转换实现的(upcasting)。C++使用的是虚成员函数满足这种需求。虚函数的底层实现是使用虚函数表来实现的。

虚函数表

​ C++在标准中没有规定虚函数的实现方式,但通常编译器是使用虚函数表来实现的。虚函数表是一个隐藏的成员,存在于每一个对象中。它是一个指针,指向一个数组,这个数组的成员是虚函数的地址。虚函数表储存了所有这个对象真正会使用的函数到底是啥。如果派生类包含了一个虚函数的新的定义,那么这个虚函数表中就会存进去新定义的函数;反之,如果没有定义新的函数,那虚函数表中保存的就是一个原来的函数。

​ 所以这就导致了这样一个问题:定义了虚函数的对象会比预想的更大!同时,对于函数的调用,还会包含一个查找地址的时间。

虚方法有什么用

这个其实是后来在各种毒打中慢慢学到的. 通常我们在设计一个基类的时候会尽量使用虚方法. 特别是当我们希望做一个接口(interface), 只提供方法声明, 不提供方法实现的时候, 虚方法就非常有用. 不仅能够让我们在调用的时候成功调用到子类的方法, 还能及时地提醒我们方法的实现.

值得注意的是, 由于构造函数需要知道对象的具体类型, 而虚函数的行为是在运行期间实际确定. 当编译器不知道对象的所有信息的时候, 就不能给他提供要给构造函数. 所以, 构造函数不能是虚函数.

而另一方面, 由于析构函数常常需要根据子类的实际情况进行析构, 所以, 析构函数常常是虚函数.