多態與虛函數:
什麼是虛函數:
用virtual關鍵字聲明的函數都是虛函數。虛函數存在的唯一目的,就是爲了實現多態(動態綁定/運行時綁定)。
virtual 關鍵字只在類定義中的成員函數聲明處使用,不能在類外部寫成員函數體時使用。所有派生類中具有覆蓋關係的同名函數都將自動成爲虛函數。
靜態成員函數不能是虛函數。
再說簡單點:有virtual聲明的函數都是虛函數。如果沒有virtual,那麼派生類中的同名函數會把基類中的同名函數隱藏了。如果有,那麼派生類的同名函數(同參同返回)會在虛函數表中將基類的同名函數覆蓋掉。
什麼是多態:
多態性可以簡單概括爲“一個接口,多種行爲”。
動態綁定(運行階段)是多態的基礎。
基類指針或引用,指向一系列的派生類對象, 調用派生類對象的同名覆蓋方法(也就是那個與基類虛函數同名同參同返回的函數),指針指向誰,就會調用誰的方法。
多態分爲兩種:
(1)編譯時多態(也叫靜態的多態):主要通過函數的重載和模板來實現。
(2)運行時多態(也叫動態的多態):主要通過虛函數來實現。
覆蓋:
基類的某個成員函數爲虛函數,派生類又定義一個成員函數,函數名、形參、返回類型都與基類的成員函數相同。那麼就會用派生類的函數覆蓋掉基類的虛函數。
說一下多態是如何實現的:
在代碼編譯階段產生一張虛函數表vftable。運行的時候,會載入內存,加載到數據段。一個類的虛函數表中列出了該類的全部虛函數地址。
如果成員裏有虛函數,在成員變量裏只會多一個虛函數指針(對象的前4個字節),指向虛函數表vftable()存放虛函數地址。虛函數的個數只會影響表的大小。不會影響對象的大小。
基類有虛函數,派生類如果有同名同參數列表同返回值的函數,派生類的函數會自動變成虛函數,在虛函數表中派生類會覆蓋掉原有的函數。
舉個栗子:
#include <iostream>
using namespace std;
class A
{
public:
int i;
virtual void func() {}
virtual void func2() {}
};
class B : public A
{
int j;
void func() {}
};
int main()
{
cout << sizeof(A) << ", " << sizeof(B);
return 0;
}
設 pa 的類型是 A*,則 pa->func() 這條語句的執行過程如下:
1) 取出 pa 指針所指位置的前 4 個字節,也就是虛函數指針。如果 pa 指向的是類 A 的對象,則這個地址就是類 A 的虛函數表的地址;類 B 同
2) 根據虛函數指針找到虛函數表,在其中查找要調用的虛函數的地址。
如果 pa 指向的是類 A 的對象,自然就會在類 A 的虛函數表中查出 A::func 的地址;類 B 同
類 B 沒有自己的 func2 函數,因此在類 B 的虛函數表中保存的是 A::func2 的地址,這樣,即便 pa 指向類 B 的對象,pa->func2();
這條語句在執行過程中也能在類 B 的虛函數表中找到 A::func2 的地址。
3) 根據找到的虛函數的地址調用虛函數。
由以上過程可以看出,只要是通過基類指針或基類引用調用虛函數的語句,就一定是多態的,也一定會執行上面的查表過程,哪怕這個虛函數僅在基類中有,在派生類中沒有。
動態綁定與靜態綁定:
綁定就是函數調用。
在使用的時候,用一個基類的指針指向了一個派生類的對象,如果調用這個虛函數,會先找虛函數指針,再找虛函數表,再再找虛函數地址。而這個綁定過程就是動態綁定(運行時期)。
如果你不用指針調用,而是用對象本身調用函數,不論是否是虛函數,都是靜態綁定(編譯時期)。
用指針調用如果是虛函數,指針識別的就是運行時期的類型;如果調用的是一般的函數,指針識別就是在編譯時期。
- 沒有virtual -> 靜態綁定
- 有 virtual 用引用或指針 -> 動態綁定
- 有 virtual 但用對象調用-> 動態綁定
純虛函數:
一般情況下,基類是不希望定義對象的。基類只是爲了將共有的屬性統一起來。
爲了實現這一目的:在基類提供的一個虛函數,爲所有派生類提供統一的虛函數接口,具體實現讓派生類自己去重寫的。
virtual void show() = 0; // 在虛函數後面加 = 0 就是純虛函數,不用去實現。
純虛函數實際上是不存在的,引入純虛函數就是爲了便於實現多態。
擁有純虛函數的類叫做抽象類。抽象類不能實例化。一般基類都應該實現爲抽象類。
那麼問題來了:
1、基類在沒有更多方法的時候,把誰實現成純虛函數呢?--------> 析構函數
首先明確一個函數想要成爲虛函數 1、它得有地址;2、得依賴對象
1)構造函數能不能是虛函數?
構造函數不依賴對象,構造函數執行完纔有對象,有對象纔有虛函數指針,所以不能是虛函數。
2)static成員方法能不能是虛函數? virtual static
靜態函數也不依賴對象,可以直接使用類名調用,也不能是虛函數。
3)inline函數能不能是虛函數? => virtual inline
內聯函數直接在程序內展開,沒有地址,無法往虛函數表放。也不能是虛函數。
4)析構函數能不能實現成虛函數?
析構函數依賴對象,有地址。可以寫成虛函數,