virtual
定義C++中虛函數的關鍵字
virtual是定義C++中虛函數的關鍵字。在面向對象程序設計領域,C++、Object Pascal 等語言中有虛函數(英語:virtual function)或虛方法(英語:virtual method)的概念。這種函數或方法可以被子類繼承和覆蓋,通常使用動態調度實現。這一概念是面向對象程序設計中(運行時)多態的重要組成部分。簡言之,虛函數可以給出目標函數的定義,但該目標的具體指向在編譯期可能無法確定。
虛函數在設計模式方面扮演重要角色。例如,《設計模式》一書中提到的23種設計模式中,僅5個對象創建模式就有4個用到了虛函數(抽象工廠、工廠方法、生成器、原型),只有單例沒有用到。
定義C++中虛函數的關鍵字
在使用virtual之前,C++對成員函數使用靜態聯編,而使用virtual,並且在調用函數時是通過指針或引用調用,C++則對成員函數進行動態聯編。
概念:
(1)以繼承為前提。
(2)在父類中用virtual修飾函數。
(3)在子類中重寫該虛函數。
作用:
(1)以父類的引用作為函數的參數類型。
(2)調用該函數傳遞子類對象。
(3)在函數中可以通過該父類的引用調用到子類中重寫的虛函數。
虛函數概念的引入可以解決這樣的問題:
在面向對象程序設計中,派生類繼承自基類。使用指針或引用訪問派生類對象時,指針或引用本身所指向的類型可以是基類而不是派生類。如果派生類覆蓋了基類中的方法,通過上述指針或引用調用該方法時,可以有兩種結果:
● ● 調用到派生類的方法:語言的運行時系統根據對象的實際類型決定,稱作“遲綁定”。
虛函數的效果屬於後者。如果問題中基類的函數是“虛”的,則調用到的都是最終派生類(英語:most-derived class)中的函數實現,與指針或引用的類型無關。反之,如果函數非“虛”,調用到的函數就在編譯期根據指針或者引用所指向的類型決定。
有了虛函數,程序甚至能夠調用編譯期還不存在的函數。
在C++中,在基類的成員函數聲明前加上關鍵字virtual即可讓該函數成為虛函數,派生類中對此函數的不同實現都會繼承這一修飾符,允許後續派生類覆蓋,達到遲綁定的效果。即便是基類中的成員函數調用虛函數,也會調用到派生類中的版本。
例如,一個基類Animal有一個虛函數eat。子類Fish要實做一個函數eat(),這個子類Fish與子類Wolf是完全不同的,但是你可以引用類別 Animal 底下的函數eat()定義,而使用子類Fish底下函數eat()的進程。
C
1 | # include |
以下是虛函數Animal::eat()的輸出:
1 |
當Animal::eat()不是被宣告為虛函數時,輸出如下所示:
1 |
在Java語言中, 所有的方法默認都是"虛函數". 只有以關鍵字 final 標記的方法才是非虛函數. 以下是 Java 中虛方法的一個例子:
1 | 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 |
輸出:
1 | 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# 的一個程序實例:
1 | 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 |
輸出:
1 | 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 | class Abstract {public: virtual void pure_virtual() = 0;}; |
純虛函數的定義僅提供方法的原型. 雖然在抽象類中通常不提供純虛函數的實現, 但是抽象類中可以包含其實現, 而且可以不在聲明的同時給出定義. 每個非抽象子類仍然需要重載該方法, 抽象類中實現的調用可以採用以下這種形式:
1 | 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++ 的構造函數中調用虛函數。而在C#、Java等另一些語言中,構造時可以調用派生類中的實現,抽象工廠模式等設計模式也鼓勵在支持這一特性的語言中使用這種用法。