面向對象程序設計領域,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).