Virtual Functions and Runtime Polymorphism in C++

C++中的虚函数及运行时多态

Posted by Tao on Thursday, March 31, 2022

本文参考自:

本文介绍了多态性和虚函数的概念,以及它们在继承中的使用。

Definitions 概念定义

  1. 虚函数是在基类中使用关键字virtual声明并由派生类重新定义(overriden)的成员函数。
  2. 多态意味着多种形式的能力。有相互继承的类可能会产生多态。

Class Hierarchy
Class Hierarchy

注意:如果我们调用一个成员函数,那么它可能会导致执行不同的函数,这取决于调用它的对象类型。

下面的简单程序是个运行时多态的小例子。该例中需要注意的是其派生类的函数是通过基类指针调用的。
重点:虚函数不是根据指针或引用的类型来调用的,而是通过对象实例指向的类型或是引用的类型决定的。
换句话说就是虚函数在运行时被延迟解析。
现在我们通过两个例子来理解一下上面晦涩的鬼话。

Without Virtual Functions

#include <iostream>
using namespace std;

// Base class
class Shape
{
public:
    Shape(int l, int w)
    {
        length = l;
        width = w;
    } // parameterized constructor
    int get_Area()
    {
        cout << "This is call to parent class area\n";
        return 1;
    }

protected:
    int length, width;
};

// Derived class
class Square : public Shape
{
public:
    Square(int l = 0, int w = 0)
        : Shape(l, w)
    {
    } // declaring and initializing derived class
    // constructor
    int get_Area()
    {
        cout << "Square area: " << length * width << '\n';
        return (length * width);
    }
};
// Derived class
class Rectangle : public Shape
{
public:
    Rectangle(int l = 0, int w = 0)
        : Shape(l, w)
    {
    } // declaring and initializing derived class
    // constructor
    int get_Area()
    {
        cout << "Rectangle area: " << length * width << '\n';
        return (length * width);
    }
};

int main()
{
    Shape *s;
    // Making object of child class Square
    Square sq(5, 5);
    // Making object of child class Rectangle
    Rectangle rec(4, 5);

    s = &sq;
    s->get_Area();
    s = &rec;
    s->get_Area();

    return 0;
}

Output:

This is call to parent class area
This is call to parent class area

上述例子中:

  • 我们把子类RectangleSquare的每个对象地址存储在s
  • 然后我们通过s调用get_Area()函数
  • 我们想的是s应该调用子类相应的get_Area()函数
  • 但实际上调用的是定义在基类上的get_Area()
  • 原因在于此时进行的静态链接(static linkage),编译器在此时只设置指定了一次基类上的get_Area()

Using Virtual Functions

得益于虚函数,我们可以创建一系列基类指针,即使不知道派生类对象的类型也可以调用派生类的方法。

Example:

我们来设计一个员工管理软件。
该软件有个基类雇员类Employee,该类有类似涨薪raiseSalary(), 调动transfer(), 晋升promote()之类的虚函数。Manager,Engineer等不同类型的雇员对于基类中的虚函数有着具体的实现。
在下面的完整例子中,我们只需要传入雇员列表并调用其合适的方法而不需要知道雇员的类型。比方说:通过遍历雇员列表,我们可以给每个雇员都涨薪。

class Employee
{
public:
    virtual void raiseSalary()
    {
        /* common raise salary code */
    }

    virtual void promote()
    { /* common promote code */
    }
};

class Manager : public Employee
{
    virtual void raiseSalary()
    {
        /* Manager specific raise salary code, may contain
        increment of manager specific incentives */
    }

    virtual void promote()
    {
        /* Manager specific promote */
    }
};

// Similarly, there may be other types of employees

// We need a very simple function
// to increment the salary of all employees
// Note that emp[] is an array of pointers
// and actual pointed objects can
// be any type of employees.
// This function should ideally
// be in a class like Organization,
// we have made it global to keep things simple
void globalRaiseSalary(Employee *emp[], int n)
{
    for (int i = 0; i < n; i++)
    {
        // Polymorphic Call: Calls raiseSalary()
        // according to the actual object, not
        // according to the type of pointer
        emp[i]->raiseSalary();
    }
}

globalRaiseSalary()一样,无需知道对象实例的类型,可以对员工列表执行许多其他操作。
虚函数非常有用,以至于后来的语言(如 Java)默认将所有方法保留为虚函数

How does the compiler perform runtime resolution? 如何实现

编译器为此目的维护了两件事:

  1. vtable:一个函数指针表,由各个类维护;
  2. vptr:指向vtable的指针,被每个对象实例维护(参考下例

VirtualFunction.png

编译器额外添加了两段代码来维护和使用vptr

  1. 每个构造函数中的代码。该代码当对象创建时设置vptr并将vptr指向该类的vtable
  2. 多态函数调用的代码(如上例的bp->show())。无论在何处进行多态调用,编译器都会插入代码以首先使用基类指针或引用查找 vptr (在上面的示例中,由于指向或引用的对象是派生类型,因此将访问派生类的vptr)。获取vptr后,即可访问派生类的vtable。使用vtable,可访问和调用派生派生类函数show()的地址。

上述方法是在C++中实现运行时多态性的标准方法吗?
C++标准并没有明确规定必须如何实现运行时多态性,但是编译器通常在相同的基本模型上做些微小的改动。

「如果这篇文章对你有用,请随意打赏」

Heisenberg Blog

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付


comments powered by Disqus