虚函数

(重定向自虛擬函式

面向对象程序设计领域,C++Object Pascal 等语言中有虚函数(英語:virtual function)或虚方法(英語:virtual method)的概念。这种函数方法可以被子类继承覆盖,通常使用动态分派实现。这一概念是面向对象程序设计中(运行时)多型的重要组成部分。简言之,虚函数可以给出目标函数的定义,但该目标的具体指向在编译期可能无法确定。

虚函数在设计模式方面扮演重要角色。例如,《设计模式》一书中提到的23种设计模式中,仅5个对象创建模式就有4个用到了虚函数(抽象工厂工厂方法生成器原型),只有单例没有用到。

目的

虚函数概念的引入可以解决这样的问题:

面向对象程序设计中,派生类继承自基类。使用指针引用访问派生类对象时,指针或引用本身所指向的类型是基类而不是派生类。如果派生类覆盖了基类中的方法,通过上述指针或引用调用该方法时,可以有两种结果:

  1. 调用到基类的方法:编译器根据指针或引用的类型决定,称作「早绑定」;
  2. 调用到派生类的方法:语言的运行时系统根据对象的实际类型决定,称作「迟绑定」。

虚函数的效果属于后者。如果问题中基类的函数是「虚」的,则调用到的都是最终派生类(英語:most-derived class)中的函数实现,与指针或引用的类型无关。反之,如果函数非「虚」,调用到的函数就在编译期根据指针或者引用所指向的类型决定。

有了虚函数,程序甚至能够调用编译期还不存在的函数。

C++ 中,在基类的成员函数声明前加上关键字 virtual 即可让该函数成为 虚函数,派生类中对此函数的不同实现都会继承这一修饰符,允许后续派生类覆盖,达到迟绑定的效果。即便是基类中的成员函数调用虚函数,也会调用到派生类中的版本。

程式範例

例如,一個基礎類別 Animal 有一個虛擬函式 eat。子類別 Fish 要實做一個函式 eat(),這個子類別 Fish 與子類別 Wolf 是完全不同的,但是你可以引用類別 Animal 底下的函式 eat() 定義,而使用子類別 Fish 底下函式 eat() 的處理程序。

C++

以下程式碼是 C++ 的程式範例。要注意的是,這個範例沒有异常處理的程式碼。尤其是 new 或是 vector::push_back 丟出一個异常時,程式在執行時有可能會出現崩溃或是錯誤的現象。

 
類別 Animal 的區塊圖
# include <iostream>
# include <vector>

using namespace std;
class Animal
{
public:
    virtual void eat() const { cout << "I eat like a generic Animal." << endl; }
    virtual ~Animal() {}
};
 
class Wolf : public Animal
{
public:
    void eat() const { cout << "I eat like a wolf!" << endl; }
};
 
class Fish : public Animal
{
public:
    void eat() const { cout << "I eat like a fish!" << endl; }
};
 
class GoldFish : public Fish
{
public:
    void eat() const { cout << "I eat like a goldfish!" << endl; }
};
 
 
class OtherAnimal : public Animal
{
};
 
int main()
{
    std::vector<Animal*> animals;
    animals.push_back( new Animal() );
    animals.push_back( new Wolf() );
    animals.push_back( new Fish() );
    animals.push_back( new GoldFish() );
    animals.push_back( new OtherAnimal() );
 
    for( std::vector<Animal*>::const_iterator it = animals.begin();
       it != animals.end(); ++it) 
    {
        (*it)->eat();
        delete *it;
    }
 
   return 0;
}

以下是虛擬函式 Animal::eat() 的輸出:

I eat like a generic Animal.
I eat like a wolf!
I eat like a fish!
I eat like a goldfish!
I eat like a generic Animal.

Animal::eat() 不是被宣告為虛擬函式時,輸出如下所示:

I eat like a generic Animal.
I eat like a generic Animal.
I eat like a generic Animal.
I eat like a generic Animal.
I eat like a generic Animal.

Java

在Java语言中, 所有的方法默认都是"虚函数". 只有以关键字 final 标记的方法才是非虚函数. 以下是 Java 中虚方法的一个例子:

import java.util.*;

public class Animal {
   public void eat() { System.out.println("I eat like a generic Animal."); }
 
   public static void main(String[] args) {
      List<Animal> animals = new LinkedList<Animal>();

      animals.add(new Animal());
      animals.add(new Wolf());
      animals.add(new Fish());
      animals.add(new OtherAnimal());

      for (Animal currentAnimal : animals) {
         currentAnimal.eat();
      }
   }
}
 
public class Wolf extends Animal {
   public void eat() { System.out.println("I eat like a wolf!"); }
}
 
public class Fish extends Animal {
   public void eat() { System.out.println("I eat like a fish!"); }
}
 
public class OtherAnimal extends Animal {}

输出:

I eat like a generic Animal.
I eat like a wolf!
I eat like a fish!
I eat like a generic Animal.

C#

在 C# 语言中, 对基类中的任何虚方法必须用 virtual 修饰, 而衍生类中由基类继承而来的重载方法必须用 override 修饰. 以下是 C# 的一个程序实例:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
  public class Animal
  {
    public virtual void Eat()
    {
      Console.WriteLine("I eat like a generic Animal.");
    }
  }

  public class Wolf : Animal
  {
    public override void Eat()
    {
      Console.WriteLine("I eat like a wolf!");
    }
  }

  public class Fish : Animal
  {
    public override void Eat()
    {
      Console.WriteLine("I eat like a fish!");
    }
  }

  public class GoldFish : Fish
  {
    public override void Eat()
    {
      Console.WriteLine("I eat like a goldfish!");
    }
  }

  public class OtherAnimal : Animal
  {
    // Eat() method is not overridden, so the base class method will be used.
  }

  public class Program
  {
    public static void Main(string[] args)
    {
      IList<Animal> animals = new List<Animal>();
      
      animals.Add(new Animal());
      animals.Add(new Wolf());
      animals.Add(new Fish());
      animals.Add(new GoldFish());
      animals.Add(new OtherAnimal());

      foreach (Animal currentAnimal in animals)
      {
        currentAnimal.Eat();
      }
    }
  }
}

输出:

I eat like a generic Animal.
I eat like a wolf!
I eat like a fish!
I eat like a goldfish!
I eat like a generic Animal.

抽象类和纯虚函数

纯虚函数纯虚方法是一个需要被非抽象的派生类覆盖(override)的虚函数. 包含纯虚方法的类被称作抽象类; 抽象类不能被直接实例化。 一个抽象基类的一个子类只有在所有的纯虚函数在该类(或其父类)内给出实现时, 才能直接实例化. 纯虚方法通常只有声明(签名)而没有定义(实现),但有特例情形要求纯虚函数必须给出函数体定义.

作为一个例子, 抽象基类"MathSymbol"可能提供一个纯虚函数 doOperation(), 和衍生类 "Plus" 和 "Minus" 提供doOperation() 的具体实现. 由于 "MathSymbol" 是一个抽象概念, 为其每个子类定义了同一的动作, 在 "MathSymbol" 类中执行 doOperation() 没有任何意义. 类似的, 一个给定的 "MathSymbol" 子类如果没有 doOperation() 的具体实现是不完全的.

虽然纯虚方法通常在定义它的类中没有实现, 在 C++ 语言中, 允许纯虚函数在定义它的类中包含其实现, 这为衍生类提供了备用或默认的行为. C++的虚基类的虚析构函数必须提供函数体定义,否则链接时(linking)在析构该抽象类的派生实例对象的语句处会报错。[1]

C++

C++语言中, 纯虚函数用一种特别的语法[=0]定义(但 VS 也支持 abstract 关键字:virtual ReturnType Function()abstract;), 见以下示例.

class Abstract {
public:
   virtual void pure_virtual() = 0;
};

纯虚函数的定义仅提供方法的原型. 虽然在抽象类中通常不提供纯虚函数的实现, 但是抽象类中可以包含其实现, 而且可以不在声明的同时给出定义[2]. 每个非抽象子类仍然需要重载该方法, 抽象类中实现的调用可以采用以下这种形式:

 void Abstract::pure_virtual() {
   // do something
 }
 
 class Child : public Abstract {
   virtual void pure_virtual(); // no longer abstract, this class may be
                                // instantiated.
 };
 
 void Child::pure_virtual() {
   Abstract::pure_virtual(); // the implementation in the abstract class 
                             // is executed
 }

构造与析构时的行为

在对象的构造函数析构函数中涉及虚函数时,不同的语言有不同的规定。在以 C++ 为代表的一些语言中,虚函数调度机制在对象的构造和析构时有不同的语义,一般建议尽可能避免在 C++ 的构造函数中调用虚函数[3]。而在 C#Java 等另一些语言中,构造时可以调用派生类中的实现,抽象工厂模式设计模式也鼓励在支持这一特性的语言中使用这种用法。

参考文献

  1. ^ MSDN "Using dllimport and dllexport in C++ Classes"页面存档备份,存于互联网档案馆): However, because a destructor for an abstract class is always called by the destructor for the base class, pure virtual destructors must always provide a definition. Note that these rules are the same for nonexportable classes.
  2. ^ Standard C++ 98 - 10.4/2
  3. ^ Meyers, Scott. Never Call Virtual Functions during Construction or Destruction. June 6, 2005 [2018-06-22]. (原始内容存档于2021-01-26).