Virtual Functions

#virtual functions #class

我们先来看以下例子。

#include <iostream>
#include <string>
using namespace std;

class Person
{
  public:
    string name;
    Person(string n): name(n){}
    void print()
    {
        cout << "Name: " << name << endl;
    }
};
class Student: public Person
{
  public:
    string id;
    Student(string n, string i): Person(n), id(i){}
    void print() 
    {
        cout << "Name: " << name;
        cout << ". ID: " << id << endl;
    }
};
void printObjectInfo(Person & p)
{
    p.print();
}

我们在父类和子类中都定义了print函数,那么此时

Person * p = new Student("xue", "2020");
p->print(); //if print() is not a virtual function, different output
delete p; //if its destructor is not virtual

p->print()肯定会调用父类的print()函数,这是符合逻辑的。类似的,

Student stu("yu", "2019");
printObjectInfo(stu);  

也会调用父类的print()函数。

但是,我们或许会希望,printObjectInfo()只是作为一个代号,调用这个函数时的输入可以是所有Person()类的派生类(子类),其中使用的函数自动识别为子类的对应函数。这种想法听起来好像天方夜谭,但实际上是可以做到的。我们只需要在父类的print()函数之前加一个virtual关键词,便可达到这样的效果,即

class Person
{
  public:
    string name;
    Person(string n): name(n){}
    virtual void print()
    {
        cout << "Name: " << name << endl;
    }
};

此时,不论是p->print();还是printObjectInfo(stu); 都会调用子类的print()函数。

这是怎么做到的呢?它的原理是,一旦出现了virtual关键词,那么每一个Person类或者其派生类的对象被创建的时候,都会自动生成一个隐藏的指针,指向函数表。这个函数表记录着此时对象所拥有的函数;virtual函数在接收对象时会根据这个隐藏指针所指向的函数调用相应的函数。因此,

  • 尽管p是一个Person类的对象,但他是通过Student的构造函数创建的,此时隐藏指针指向的就是Student::print();
  • 类似的,虽然printObjectInfo()的输入参数是一个Person对象的引用,但是它是通过Student对象初始化的,因此他的隐藏指针指向的也是Student::print().

事实上,所有析构函数(destructor)都是virtual函数,这样在释放内存的时候才不会出错。

本节的视频讲解参见Bilibili - 于仕琪老师的C/C++从基础语法到优化策略课程第12.4节。