C++的虛函數是其實作多态的基礎,今天在這裡分享一下我對C++虛函數相關知識的系統總結,技術有限,如有不當,歡迎指正。
在将内容前,将大緻涉及到的内容圖解如下:
1. 有無虛函數在繼承中的差別
//-- Zuo add on 2018-04-07
class A
{
public:
virtual void fun(){ std::cout << "A::fun"; }
void fun1(){ std::cout << "A::fun1"; }
};
class B : public A
{
public:
virtual void fun(){ std::cout << "B::fun"; }
void fun1(){ std::cout << "B::fun1"; }
};
void main(char argc, char** argv)
{
A *a = new A;
A *b = new B;
a->fun();
b->fun();
a->fun1();
b->fun1();
}
輸出結果:
A::fun
B::fun
A::fun1
A::fun1
可以看到,如果是虛函數,在繼承的時候,如果子類有重寫,就會被覆寫(由子類的實作代替父類的實作),如果是普通成員函數,就不會被子類的實作覆寫,這也就是多态的原型。
例子很簡單,想必大家很容易看懂,但是這裡我提出兩個問題大家思想下,後面會有答案:
1.1. 如果将上文的
A *a = new B;
修改成
B *a = new B;
結果會怎樣變化?
1.2. 如果虛函數有預設值,預設值會有什麼特點?
2. 虛函數的本質
虛函數的本質是因為C++給有虛函數的類增加了一張虛函數表,用來存儲所有虛函數的入口位址。在子類繼承父類的過程中,首先繼承的是這張虛函數表Virtual-Table(以下簡稱VT),如果子類有對父類虛函數的重寫,那麼就會在VT中覆寫對應的函數位址。這樣就可以實作同樣的調用,在不同的子類裡,有不同的實作,這也就是多态。
注意一個問題,VT是跟着類走的,也就是說,如果是上文中1.1提到的,那麼VT的覆寫就不會發生,因為
A *a = new B;
等價于
A *a = (A*)new B;
正因為VT是跟着類走的,如果是
B *a = new B;
,那就無任何特别,普通的建立對象,如果是
A *a = new B;
,那在将B類轉換成A類的時候,也就是VT合并的時候。是以上文的1.1,輸出值就和
class A
毫無關系了。
除了上面講到的,還有兩點特性:
2.1. 隻有虛函數的入口位址才會被存儲在VT中,如果是普通成員函數,當然不會存儲在裡面。
2.2. 為了提高虛函數的調用效率,VT的位址被存放在類的最前面。
我從網上找了一張圖比較明了:
在繼承的過程在VT被子類重寫的虛函數位址覆寫父類的虛函數位址,也就是同一個指針在不同的對象中可以指向不同的函數實作,這也就是虛函數的動态綁定實作多态的過程了。這個可以和普通函數的靜态綁定相對比,普通函數是在編譯期就靜态綁定了,而虛函數是在運作期通過VT存儲的函數位址實作動态綁定。
3. 純虛函數
純虛函數是一種比虛函數更加極端的函數。它的形式如下:
virtual void fun() = 0;
它存在的目的是為了規範接口,使得子類必須要實作對應的接口,如果子類沒有實作接口,則會編譯報錯。
使用純虛函數要注意一點,包含純虛函數的類被稱為抽象類,一般被設計為基類,且抽象類不能被執行個體化(因為有未實作的純虛函數)。
4. 安全性-通路non-public虛函數
VT的存在固然為實作C++的多态立下汗馬功勞,但是凡事都有雙面性,它的到來,也引入了C++的一些安全上的不足。上文我們說到了,為了提高多态調用的性能,C++将VT位址存放在類空間的段首位置,是以我們通過擷取類的位址可以找到VT的位址,也就是可以得到一個類所有虛函數的位址,那如果,這裡面的虛函數有是non-public的,那就破壞了C++的封裝屬性了。Show Code:
//-- Zuo add on 2018-04-07
class A
{
private:
virtual void fun(){ qDebug() << "A fun"; }
};
void main(char argc, char** argv)
{
A *a = new A;
typedef void(*fun)(void);
std::cout << "虛函數表位址 = " << (int*)(a) << std::endl;
std::cout << "第一個虛函數位址 = " << (int*)(*(int*)a << std::endl;
//-- 将虛函數位址轉換為void fun(void)函數指針
fun f = (fun)*((int*)(*(int*)a));
f();
}
可以看到,這裡可以無報錯的通路到原本為private的虛函數。
5. 虛函數的預設值不能被覆寫
虛函數雖然可以被子類所覆寫以形成多态,但是有一個細節還是要注意,虛函數的預設值是不能被覆寫的,還是上面的代碼:
//-- Zuo add on 2018-04-07
class A
{
public:
virtual void fun(int a = ){ std::cout << "A::fun && a = " << a; }
};
class B : public A
{
public:
virtual void fun(int a = ){ std::cout << "B::fun && a = " << a; }
};
void main(char argc, char** argv)
{
A *a = new A;
A *b = new B;
a->fun();
b->fun();
}
輸出結果:
A::fun && a =
B::fun && a =
可以看到這裡的
a
沒有被改變。