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++ 將靜態綁定設爲默認選項主要有一下兩方面考慮:
- 效率
爲使程序能夠在運行階段進行決策,必須採用一些方法來跟蹤基類指針或引用指向的對象類型,這將增加額外的處理開銷(也就是動態綁定)。如果派生類不重新定義基類的方法,也就不需要動態綁定,此時,使用靜態綁定更合理,更高效。 - 概念模型
在設計類時,可能包含一些不在或者不希望在派生類中重新定義的成員函數,就不將其設計成虛函數。這樣也表明,僅那些預期將要被重新定義的方法聲明爲虛的。
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();
};
- 在
Person
基類定義了虛函數aboutMe()
,在Student
中併爲重新定義。 - 在
Person
基類定義了虛函數saySomething()
,並在Student
中重新定義saySomething()
。 - 在
Student
繼承類中添加了新的虛函數learnSomething()
。
實際上,無論類中包含的虛函數是1個還是多個,都只需要在對象中添加一個地址成員,只是表的大小不同而已。
6. 注意
- 虛方法是繼承的,一旦在基類中將每個方法聲明爲虛方法,則在子類中不可能再把它聲明爲非虛方法了。
- 析構器都是虛方法,可以防止內存泄漏。
- 構造器都不是虛方法。創建派生類對象時,將調用派生類的構造函數,而不是基類的構造函數,然後派生類的構造函數將使用基類的構造函數,這種順序不同於繼承機制,因此,派生類不繼承基類的構造函數。
- 使用虛函數時,在內存和執行速度方面有一定的成本:包括
- 每個對象都將增大,增大量爲存儲地址的空間
- 對於每個類,編譯器都創建一個虛函數地址表
- 對於每個虛函數調用,都需要執行一些額外操作,即到表中查找地址。
- 如果要在派生類中重新定義基類的方法,則將它設置爲虛方法,否則設置爲非虛方法。
7. 參考
《C++ Primer Plus》13.4 靜態聯編和動態聯編
《程序員面試金點》8.13