摘自《C++ Primer Plus》第6版13.3ios
示例:c++
#include<string> class Brass { private: std::string fullName; long acctNum; double balance; public: Brass(const std::string & s= "NullBody", long an=-1, double bal =0.0); void Despoit(double amt); virtual void Withdraw(double amt); double Balance() const; virtual void ViewAcct() const; virtual ~Brass(){} }; class BrassPlus: public Brass { private: double maxLoan; double rate; double owesBank; public: BrassPlus(const std::string & s= "NullBody", long an=-1, double bal =0.0, double ml=500, double r=0.1125); BrassPlus(const Brass & ba, double ml=500,double r=0.1125); virtual void ViewAcct() const; virtual void Withdraw(double amt); void ResetMax(double m) {maxLoan=m;} };
類Brass中加了Virtual關鍵字的函數就是虛函數 。程序員
若是方法是經過引用類型或指針而不是對象調用的,它將肯定使用哪種方法。若是沒有使用關鍵字virtual,程序將根據引用類型或指針類型選擇方法;若是使用了virtual,程序將根據引用或指針指向的對象的類型來選擇方法。編程
對於一個函數ViewAcct()來講,若是ViewAcct()不是虛的,則程序的行爲以下:dom
// behavior with non-virtual ViewAcct() // method chosen according to reference type Brass dom("Dominic Banker", 11224, 4183.45); BrassPlus dot("Dorothy Banker", 12118, 2592.00); Brass & b1_ref = dom; Brass & b2_ref = dot; b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use Brass::ViewAcct()
引用變量的類型爲Brass,因此選擇了Brass::ViewAccount()。
使用Brass指針代替引用時,行爲將與此相似。
若是ViewAcct()是虛的,則行爲以下:編程語言
// behavior with virtual ViewAcct() // method chosen according to object Brass dom("Dominic Banker", 11224, 4183.45); BrassPlus dot("Dorothy Banker", 12118, 2592.00); Brass & b1_ref = dom; Brass & b2_ref = dot; b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use BrassPlus::ViewAcct()
這裏兩個引用的類型都是Brass,但b2_ref引用的是一個BrassPlus對象,因此使用的是BrassPlus::ViewAcct()。使用Brass指針代替引用時,行爲將相似。函數
基類聲明瞭一個虛析構函數。這樣作是爲了確保釋放派生對象時,按正確的順序調用析構函數。spa
#include <iostream> using namespace std; class GrandFather { public: GrandFather() {} virtual void fun() { cout << "GrandFather call function!" << endl; } virtual ~GrandFather() { cout << "GrandFather destruction!" << endl; } }; class Father : public GrandFather { public: Father() {} void fun() { cout << "Father call function!" << endl; } ~Father() { cout << "Father destruction!" << endl; } }; class Son : public Father { public: Son() {} void fun() { cout << "Son call function!" << endl; } ~Son() { cout << "Son destruction!" << endl; } }; void print(GrandFather* p) { p->fun(); } int main(int argc, char* argv[]) { Father * pfather = new Son; delete pfather; return 0; }
運行結果:設計
Son destruction! Father destruction! GrandFather destruction!
若是基類 GrandFather 的析構函數不爲虛,則執行結果爲:指針
Father destruction! GrandFather destruction!
爲什麼須要虛析構函數
在程序清單13.10中,使用delete釋放由new分配的對象的代碼說明了爲什麼基類應包含一個虛析構函數,雖然有時好像並不須要析構函數。若是析構函數不是虛的,則將只調用對應與指針類型的析構函數。對於程序清單13.10,這意味着只有Brass的析構函數被調用,即便指針指向的是一個BrassPlus對象。若是析構函數是虛的,將調用相應對象類型的析構函數。所以,若是指針指向的是BrassPlus對象,將調用BrassPlus的析構函數,而後自動調用基類的析構函數。所以,使用虛析構函數能夠確保正確的析構函數序列被調用!
C++語言爲咱們提供了一種語法結構,經過它能夠指明,一個虛擬函數只是提供了一個可被子類型改寫的接口。可是,它自己並不能經過虛擬機制被調用。這就是純虛函數(pure virtual function)。 純虛函數的聲明以下所示:
class BaseEllipse //abstract base class { private: double x; //x-coordinate of the ellipse's center double y; //y-coordinate of the ellipse's center ....... public: BaseEllipse(double x0 = 0,double y0 = 0): x(x0), y(y0) {} virtual ~BaseEllipse() {} void Move(int nx, int ny){x = nx; y = ny;} virtual double Area() const =0; //a pure virual function ..... }
當類聲明中包含純虛函數時,則不能建立該類的對象。這裏的理念是,包含純虛函數的類只用做基類。原型中的=0使虛函數成爲純虛函數。這裏的方法Area()沒有定義,但c++甚至容許純虛函數有定義。例如,也許全部的基類方法都與Move()同樣,能夠在基類中進行定義,但你仍須要將這個類聲明爲抽象的。在這種狀況下,能夠將原型聲明爲虛的:
virtual void Move(int nx, int ny) = 0;
這將使基類成爲抽象的,但你仍能夠在實現文件中提供方法的定義:
void BaseEllipse::Move(int nx, int ny) {x = nx; y = ny;}
總之在原型中使用=0指出類是一個抽象基類,在類中能夠不定義該函數。
總結:抽象類只能做爲基類來使用,其純虛函數的實現由派生類給出。若是派生類沒有從新定義純虛函數,而派生類只是繼承基類的純虛函數,則這個派生類仍然仍是一個抽象類。若是派生類中給出了基類純虛函數的實現,則該派生類就再也不是抽象類了,它是一個能夠創建對象的具體類了。
虛繼承主要解決多重繼承帶來的問題。
爲了解決多繼承時的命名衝突和冗餘數據問題,C++ 提出了虛繼承,使得在派生類中只保留一份間接基類的成員。
在繼承方式前面加上 virtual 關鍵字就是虛繼承,請看下面的例子:
//間接基類A class A{ protected: int m_a; }; //直接基類B class B: virtual public A{ //虛繼承 protected: int m_b; }; //直接基類C class C: virtual public A{ //虛繼承 protected: int m_c; }; //派生類D class D: public B, public C{ public: void seta(int a){ m_a = a; } //正確 void setb(int b){ m_b = b; } //正確 void setc(int c){ m_c = c; } //正確 void setd(int d){ m_d = d; } //正確 private: int m_d; }; int main(){ D d; return 0; }
這段代碼使用虛繼承從新實現了上圖所示的菱形繼承,這樣在派生類 D 中就只保留了一份成員變量 m_a,直接訪問就不會再有歧義了。
虛繼承的目的是讓某個類作出聲明,承諾願意共享它的基類。其中,這個被共享的基類就稱爲虛基類(Virtual Base Class),本例中的 A 就是一個虛基類。在這種機制下,不論虛基類在繼承體系中出現了多少次,在派生類中都只包含一份虛基類的成員。
如今讓咱們從新梳理一下本例的繼承關係,以下圖所示:
圖1:使用虛繼承解決菱形繼承中的命名衝突問題
觀察這個新的繼承體系,咱們會發現虛繼承的一個不太直觀的特徵:必須在虛派生的真實需求出現前就已經完成虛派生的操做。在上圖中,當定義 D 類時纔出現了對虛派生的需求,可是若是 B 類和 C 類不是從 A 類虛派生獲得的,那麼 D 類仍是會保留 A 類的兩份成員。
換個角度講,虛派生隻影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類自己。
在實際開發中,位於中間層次的基類將其繼承聲明爲虛繼承通常不會帶來什麼問題。一般狀況下,使用虛繼承的類層次是由一我的或者一個項目組一次性設計完成的。對於一個獨立開發的類來講,不多須要基類中的某一個類是虛基類,何況新類的開發者也沒法改變已經存在的類體系。
C++標準庫中的 iostream 類就是一個虛繼承的實際應用案例。iostream 從 istream 和 ostream 直接繼承而來,而 istream 和 ostream 又都繼承自一個共同的名爲 base_ios 的類,是典型的菱形繼承。此時 istream 和 ostream 必須採用虛繼承,不然將致使 iostream 類中保留兩份 base_ios 類的成員。
圖2:虛繼承在C++標準庫中的實際應用
由於在虛繼承的最終派生類中只保留了一份虛基類的成員,因此該成員能夠被直接訪問,不會產生二義性。此外,若是虛基類的成員只被一條派生路徑覆蓋,那麼仍然能夠直接訪問這個被覆蓋的成員。可是若是該成員被兩條或多條路徑覆蓋了,那就不能直接訪問了,此時必須指明該成員屬於哪一個類。
以圖1中的菱形繼承爲例,假設 B 定義了一個名爲 x 的成員變量,當咱們在 D 中直接訪問 x 時,會有三種可能性:
能夠看到,使用多繼承常常會出現二義性問題,必須十分當心。上面的例子是簡單的,若是繼承的層次再多一些,關係更復雜一些,程序員就很容易陷人迷魂陣,程序的編寫、調試和維護工做都會變得更加困難,所以我不提倡在程序中使用多繼承,只有在比較簡單和不易出現二義性的狀況或實在必要時才使用多繼承,能用單一繼承解決的問題就不要使用多繼承。也正是因爲這個緣由,C++ 以後的不少面向對象的編程語言,例如 Java、C#、PHP 等,都不支持多繼承。
virtual可用來定義類函數和應用到虛繼承。
友元函數, 構造函數不能用virtual關鍵字修飾;
普通成員函數和析構函數能夠用virtual關鍵字修飾;