C++ 虛函數

1. 什麼是虛函數

在某基類中聲明爲virtual 並在一個或多個派生類中被重新定 義的成員函數,用法格式爲:

virtual 函數返回類型 函數名(參數表) {函數體};


virtual void aboutMe()
{
    cout << "I am a person." << endl;
}

簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性,多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異,而採用不同的策略。

2. 爲什麼要用虛函數

看如下代碼,是一個 Person 的基類和一個Student的繼承類。

class Person
{
    int id; // 所有成員默認爲私有(Private)
    char name[50];
public:
    void aboutMe()
    {
        cout << "I am a person." << endl;
    }
};

class Student : public Person
{
public:
    void aboutMe()
    {
        cout << "I am a student." << endl;
    }
};

定義一個Student類型的指針變量:

Student *p1 = new Student();
p1->aboutMe(); // 打印輸出“I am a student.”

如果把上面的 p1 定義爲 Person *又會如何?

Person *p2 = new Student();
p2->aboutMe(); // 打印輸出“I am a person.”

p2明明是指向一個Student類型的對象,爲什麼p2->aboutMe()的輸出會是“I am a person.”,也就是調用Person基類的aboutMe()方法呢?

這是因爲指針類型在編譯時已知,因此編譯器在編譯的時候,將p2->aboutMe()中的aboutMe()關聯到了Person::aboutMe(),也就是所謂的靜態綁定(static binding)機制。也就是說編譯器在編譯的時候就告訴p2->aboutMe(),等到執行你的時候就直接執行Person基類中的aboutMe()方法。

3. 爲什麼使用靜態綁定

因爲靜態綁定是 c++ 的默認選項,實際上還有動態綁定,編譯器對虛函數使用的就是動態綁定。

c++ 將靜態綁定設爲默認選項主要有一下兩方面考慮:

  1. 效率
    爲使程序能夠在運行階段進行決策,必須採用一些方法來跟蹤基類指針或引用指向的對象類型,這將增加額外的處理開銷(也就是動態綁定)。如果派生類不重新定義基類的方法,也就不需要動態綁定,此時,使用靜態綁定更合理,更高效。
  2. 概念模型
    在設計類時,可能包含一些不在或者不希望在派生類中重新定義的成員函數,就不將其設計成虛函數。這樣也表明,僅那些預期將要被重新定義的方法聲明爲虛的。

4. 使用虛函數

還是以上面的兩個類爲例,修改Person類如下所示:

class Person
{
    ...
    virtual void aboutMe()  // 僅更新此處
    {
        cout << "I am a person." << endl;
    }
};

此時:

Person *p2 = new Student();
p2->aboutMe(); // 打印輸出“I am a student.”

virtual告訴編譯器,根據指針在運行時的類型有選擇的調用正確的方法。也就是說p2->aboutMe()是調用Person::aboutMe() 還是Student::aboutMe()是在運行的時候根據p2指針的類型決定的。

5. 虛函數的工作原理

通常,編譯器處理虛函數的方法是:給每一個對象添加一個隱藏的指針成員(vptr)。隱藏成員保存了一個指向函數地址數組的指針。這種數組稱爲虛函數表。

虛函數表中存儲了爲類對象進行聲明的虛函數的地址。

例如:

基類對象包含一個指針,該指針指向基類中所有虛函數的地址表。派生對象將包含一個指向對立地址表的指針,如果派生對象提供了虛函數的新定義,該虛函數表將保存新函數的地址;如果派生類定義了新的虛函數,則該新函數的地址也將添加到虛函數表中。

看如下代碼:

class Person
{
    int id; // 所有成員默認爲私有(Private)
    char name[50];
public:
    virtual void aboutMe();
    virtual void saySomething();
};

class Student : public Person
{
    char school[50];
public:
    void saySomething();
    virtual void learnSomthing();
};
  1. Person基類定義了虛函數aboutMe(),在Student中併爲重新定義。
  2. Person基類定義了虛函數saySomething(),並在Student中重新定義saySomething()
  3. Student繼承類中添加了新的虛函數learnSomething()

這裏寫圖片描述

實際上,無論類中包含的虛函數是1個還是多個,都只需要在對象中添加一個地址成員,只是表的大小不同而已。

6. 注意

  1. 虛方法是繼承的,一旦在基類中將每個方法聲明爲虛方法,則在子類中不可能再把它聲明爲非虛方法了。
  2. 析構器都是虛方法,可以防止內存泄漏。
  3. 構造器都不是虛方法。創建派生類對象時,將調用派生類的構造函數,而不是基類的構造函數,然後派生類的構造函數將使用基類的構造函數,這種順序不同於繼承機制,因此,派生類不繼承基類的構造函數。
  4. 使用虛函數時,在內存和執行速度方面有一定的成本:包括
    1. 每個對象都將增大,增大量爲存儲地址的空間
    2. 對於每個類,編譯器都創建一個虛函數地址表
    3. 對於每個虛函數調用,都需要執行一些額外操作,即到表中查找地址。
  5. 如果要在派生類中重新定義基類的方法,則將它設置爲虛方法,否則設置爲非虛方法。

7. 參考

《C++ Primer Plus》13.4 靜態聯編和動態聯編

《程序員面試金點》8.13

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章