C++ 虛函數詳解

虛函數

C++中的虛函數的做用主要是實現了多態的機制。關於多態,簡而言之就是用父類型別的指針指向其子類的實例,而後經過父類的指針調用實際子類的成員函數。這種技術可讓父類的指針有「多種形態」。ios

虛函數的工做原理

編譯器必須生成可以在程序運行時選擇正確的虛方法的代碼,這被稱爲動態聯編數組

當類中存在虛函數時,編譯器默認會給對象添加一個隱藏成員。該成員爲一個指向虛函數表(virtual function table,vtbl)的指針。安全

虛函數表是一個保存了虛函數地址的數組。編譯器會檢查類中全部的虛函數,依次將每一個虛函數的地址,存入虛函數表。函數

虛函數表主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了這個實例的內存中,因此,當咱們用父類的指針來操做一個子類的時候,這張虛函數表就顯得由爲重要了,它就像一個地圖同樣,指明瞭實際所應該調用的函數。
沒有覆蓋父類的虛函數是毫無心義的。spa

通常繼承(無虛函數覆蓋)
#include <iostream>

using namespace std;

class Base
{
public:
    virtual void f()
    {
        cout << "Base::f" << endl;
    }
    virtual void g()
    {
        cout << "Base::g" << endl;
    }
};

class Derive : public Base
{

};

int main()
{
    Base *b = new Derive();
    b->f();
}

//打印結果爲:
Base::f

image

虛函數按照其聲明順序放於表中,父類的虛函數在子類的虛函數前面。設計

通常繼承(有虛函數覆蓋)
#include <iostream>

using namespace std;

class Base
{
public:
    virtual void f()
    {
        cout << "Base::f" << endl;
    }
    virtual void g()
    {
        cout << "Base::g" << endl;
    }
    virtual void h()
    {
        cout << "Base::h" << endl;
    }
};

class Derive : public Base
{
    virtual void f()
    {
        cout << "Derive::f" << endl;
    }
    virtual void g1()
    {
        cout << "Derive::g1" << endl;
    }
    virtual void h1()
    {
        cout << "Derive::h1" << endl;
    }
};

int main()
{
    Base *b = new Derive();
    b->f();
}

//打印結果爲:
Derive::f

image

派生類覆蓋父類虛函數的函數被放到了虛表中原來父類虛函數的位置。沒有被覆蓋的函數依舊。指針

多重繼承(無虛函數覆蓋)

image

每一個父類都有本身的虛表。code

子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)對象

這樣作就是爲了解決不一樣的父類類型的指針指向同一個子類實例,而可以調用到實際的函數。blog

多重繼承(有虛函數覆蓋)

image

個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,咱們就能夠任一靜態類型的父類來指向子類,並調用子類的f()了。

總結:

  • 1, 虛函數是非靜態的、非內聯的成員函數,而不能是友元函數,但虛函數能夠在另外一個類中被聲明爲友元函數。
  • 2, 虛函數聲明只能出如今類定義的函數原型聲明中,而不能在成員函數的函數體實現的時候聲明。
  • 3, 一個虛函數不管被公有繼承多少次,它仍然保持其虛函數的特性。
  • 4, 若類中一個成員函數被說明爲虛函數,則該成員函數在派生類中可能有不一樣的實現。當使用該成員函數操做指針或引用所標識的對象時 ,對該成員函數調用可採用動態聯編。
  • 5, 定義了虛函數後,程序中聲明的指向基類的指針就能夠指向其派生類。在執行過程當中,該函數能夠不斷改變它所指向的對象,調用不一樣 版本的成員函數,並且這些動做都是在運行時動態實現的。虛函數充分體現了面向對象程序設計的動態多態性。 純虛函數 版本的成員函數,並且這些動做都是在運行時動態實現的。虛函數充分體現了面向對象程序設計的動態多態性。
純虛函數

虛函數的做用是容許在派生類中從新定義與基類同名的函數,而且能夠經過基類指針或引用來訪問基類和派生類中的同名函數。

1, 當在基類中不能爲虛函數給出一個有意義的實現時,能夠將其聲明爲純虛函數,其實現留待派生類完成。

2, 純虛函數的做用是爲派生類提供一個一致的接口。
純虛函數不能實例化,但能夠聲明指針。

類中至少有一個函數被聲明爲純虛函數,則這個類就是抽象類。抽象類不能被用於實例化對象,它只能做爲接口使用。

虛基類 和 虛繼承

image

#include <iostream>

using namespace std;

class A
{
public:
    int iValue;
};

class B :public A
{
public:
    void bPrintf(){ cout << "This is class B" << endl; };
};

class C :public A
{
public:
    void cPrintf(){ cout << "This is class C" << endl; };
};

class D :public B, public C
{
public:
    void dPrintf(){ cout << "This is class D" << endl; };
};

void main()
{
    D d;
//    cout << d.iValue << endl; //錯誤,不明確的訪問
    cout << d.A::iValue << endl; //正確
    cout << d.B::iValue << endl; //正確
    cout << d.C::iValue << endl; //正確
}

類B C都繼承了類A的iValue成員,所以類B C都有一個成員變量iValue ,而類D又繼承了B C,這樣類D就有一個重名的成員 iValue(一個是從類B中繼承過來的,一個是從類C中繼承過來的).在主函數中調用d.iValue 由於類D有一個重名的成員iValue編譯器不知道調用 從誰繼承過來的iValue因此就產生的二義性的問題.正確的作法應該是加上做用域限定符 d.B::iValue 表示調用從B類繼承過來的iValue。不過 類D的實例中就有多個iValue的實例,就會佔用內存空間。因此C++中就引用了虛基類的概念,來解決這個問題。

class A  
{  
public:  
 int iValue;  
};  
  
class B:virtual public A  
{  
public:  
 void bPrintf(){cout<<"This is class B"<<endl;};  
};  
  
class C:virtual public A  
{  
public:  
 void cPrintf(){cout<<"This is class C"<<endl;};  
};  
  
class D:public B,public C  
{  
public:  
 void dPrintf(){cout<<"This is class D"<<endl;};  
};  
  
void main()  
{  
 D d;  
 cout<<d.iValue<<endl; //正確  
}

image
在繼承的類的前面加上virtual關鍵字表示被繼承的類是一個虛基類,它的被繼承成員在派生類中只保留一個實例。例如iValue這個成員,從類 D這個角度上來看,它是從類B與類C繼承過來的,而類B C又是從類A繼承過來的,但它們只保留一個副本。所以在主函數中調用d.iValue時就不 會產生錯誤。

總結:

  • 1, 一個類能夠在一個類族中既被用做虛基類,也被用做非虛基類。
  • 2, 在派生類的對象中,同名的虛基類只產生一個虛基類子對象,而某個非虛基類產生各自的子對象。
  • 3, 虛基類子對象是由最派生類的構造函數經過調用虛基類的構造函數進行初始化的。
  • 4, 最派生類是指在繼承結構中創建對象時所指定的類。
  • 5, 派生類的構造函數的成員初始化列表中必須列出對虛基類構造函數的調用;若是未列出,則表示使用該虛基類的缺省構造函數。
  • 6, 從虛基類直接或間接派生的派生類中的構造函數的成員初始化列表中都要列出對虛基類構造函數的調用。但只有用於創建對象的最派生 類的構造函數調用虛基類的構造函數,而該派生類的全部基類中列出的對虛基類的構造函數的調用在執行中被忽略,從而保證對虛基類子對象 只初始化一次。
  • 7, 在一個成員初始化列表中同時出現對虛基類和非虛基類構造函數的調用時,虛基類的構造函數先於非虛基類的構造函數執行。

析構函數與虛函數

將析構函數定義爲虛函數主要緣由是由於多態的存在。

#include <iostream>

class Base
{
public:
    Base(){ std::cout << "Constructing Base!" << std::endl; };
    ~Base() { std::cout << "Destroy Base!" << std::endl; };
};

class Derive : public Base
{
public:
    Derive(){ std::cout << "Constructing Derive!" << std::endl; };
    ~Derive() { std::cout << "Destroy Derive!" << std::endl; };
};

int main()
{
    Base *basePtr = new Derive();

    delete basePtr;
    return 0;
}

//打印結果爲:
Constructing Base!
Constructing Derive!
Destroy Base!

//只刪除了基類的分配的空間,派生類的對象的空間沒有刪除,會形成內存泄漏。

析構函數應是虛函數,除非類不用作基類。

由虛函數表,咱們知道,若析構函數不聲明爲virtual,則調用的將是Base類的析構函數,而沒有調用Derive類的析構函數,此時形成了內存泄露。

因此析構函數必須聲明爲虛函數,調用的將是子類Derive的析構函數,

咱們還須要知道的一點是,子類析構函數,必定會調用父類析構函數,釋放父類對象,則內存安全釋放。
析構函數的調用順序爲先調用派生類析構函數清理新增的成員,再調用子對象析構函數(基類析構函數)清理子對象,最後再調用基類析構函數清理基類成員。

#include <iostream>

class Base
{
public:
    Base(){ std::cout << "Constructing Base!" << std::endl; };
    virtual ~Base() { std::cout << "Destroy Base!" << std::endl; };
};

class Derive : public Base
{
public:
    Derive(){ std::cout << "Constructing Derive!" << std::endl; };
    ~Derive() { std::cout << "Destroy Derive!" << std::endl; };
};

int main()
{
    Base *basePtr = new Derive();

    delete basePtr;
    return 0;
}

//打印結果:
Constructing Base!
Constructing Derive!
Destroy Derive!
Destroy Base!

構造函數爲何不能是虛函數

1. 從存儲空間角度,虛函數對應一個指向vtable虛函數表的指針,這你們都知道,但是這個指向vtable的指針實際上是存儲在對象的內存空間的。問題出來了,若是構造函數是虛的,就須要經過 vtable來調用,但是對象尚未實例化,也就是內存空間尚未,怎麼找vtable呢?因此構造函數不能是虛函數。

2. 從使用角度,虛函數主要用於在信息不全的狀況下,能使重載的函數獲得對應的調用。構造函數自己就是要初始化實例,那使用虛函數也沒有實際意義呀。因此構造函數沒有必要是虛函數。虛函數的做用在於經過父類的指針或者引用來調用它的時候可以變成調用子類的那個成員函數。而構造函數是在建立對象時自動調用的,不可能經過父類的指針或者引用去調用,所以也就規定構造函數不能是虛函數。

3. 構造函數不須要是虛函數,也不容許是虛函數,由於建立一個對象時咱們老是要明確指定對象的類型,儘管咱們可能經過實驗室的基類的指針或引用去訪問它但析構卻不必定,咱們每每經過基類的指針來銷燬對象。這時候若是析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。

4. 從實現上看,vbtl在構造函數調用後才創建,於是構造函數不可能成爲虛函數從實際含義上看,在調用構造函數時還不能肯定對象的真實類型(由於子類會調父類的構造函數);並且構造函數的做用是提供初始化,在對象生命期只執行一次,不是對象的動態行爲,也沒有必要成爲虛函數。

5. 當一個構造函數被調用時,它作的首要的事情之一是初始化它的VPTR。所以,它只能知道它是「當前」類的,而徹底忽視這個對象後面是否還有繼承者。當編譯器爲這個構造函數產生代碼時,它是爲這個類的構造函數產生代碼——既不是爲基類,也不是爲它的派生類(由於類不知道誰繼承它)。因此它使用的VPTR必須是對於這個類的VTABLE。並且,只要它是最後的構造函數調用,那麼在這個對象的生命期內,VPTR將保持被初始化爲指向這個VTABLE, 但若是接着還有一個更晚派生的構造函數被調用,這個構造函數又將設置VPTR指向它的 VTABLE,等.直到最後的構造函數結束。VPTR的狀態是由被最後調用的構造函數肯定的。這就是爲何構造函數調用是從基類到更加派生類順序的另外一個理由。可是,當這一系列構造函數調用正發生時,每一個構造函數都已經設置VPTR指向它本身的VTABLE。若是函數調用使用虛機制,它將只產生經過它本身的VTABLE的調用,而不是最後的VTABLE(全部構造函數被調用後纔會有最後的VTABLE)。

相關文章
相關標籤/搜索