虛函數出現的原因
C++多態通過虛函數來實現,虛函數允許子類重新定義成員函數,而子類重新定義父類的做法稱爲覆蓋,或者稱爲重寫。
最常見的用法就是聲明基類的指針,利用該指針指向任意一個子類對象,調用相應的虛函數,動態綁定。由於編寫代碼時不能確定被調用的是基類還是哪個派生類的函數,所以被稱爲“虛函數”。如果沒有使用虛函數的話,即沒有利用C++的多態性,則利用基類指針調用相應的函數時,總被限制在基類函數本身,而無法調用到子類中被重寫過的函數。
#include<iostream>
using namespace std;
class A
{
public:
void foo()
{
printf("A::foo()1\n");
}
virtual void fun()
{
printf("A::fun 2\n");
}
};
class B : public A
{
public:
void foo() //隱藏:派生類的函數屏蔽了與其同名的基類函數
{
printf("B::foo3\n");
}
void fun() //多態、覆蓋
{
printf("B::fun4\n");
}
};
int main(void)
{
A a;
B b;
A *p = &a;
p->foo(); //輸出1
p->fun(); //輸出2
p = &b;
p->foo(); //取決於指針類型,輸出1
p->fun(); //取決於對象類型,輸出4,體現了多態
return 0;
}
虛函數調用:在運行時確定,取決於指針指向實例的類型。
C++虛函數的實現機制
虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實現的。簡稱爲V-Table。在這個表中,主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函數C++的編譯器應該是保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。 這意味着我們通過對象實例的地址得到這張虛函數表,然後就可以遍歷其中函數指針,並調用相應的函數。虛函數表是類對象之間共享的,而非每個對象都保存了一份。
虛函數的地址存放於虛函數表之中。運行期間多態就是通過虛函數和虛函數表實現的。類的對象內部會有指向類內部的虛表地址的指針。通過這個指針調用虛函數。
虛函數在c++中的實現機制就是用虛表和虛指針。每個類用了一個虛表,每個類的對象用了一個虛指針。指向虛表。
具體用法如下:
class A{
public:
virtual void f();
virtual void g();
private:
int a;
};
class B:public A{
public:
void g();
private:
int b;
};
因爲A由virtual void f()和g(),所以編譯器爲A類準備了一個虛表vtableA,內容如下:
B因爲繼承了A,所以編譯器也爲B準備了一個虛表vtableB,內容如下
注意:因爲B::g是重寫了的,所以B的虛表的g存放的是B::g的入口地址,但是f是從A繼承下來的,所以f的地址是A::f的入口地址。
實例化B, B bB;編譯器分配空間時,除了A的int a,B的成員int b以外還分配了一個虛指針vptr,指向B的虛表vtableB,bB的佈局如下:
當如下語句時
A *pa = &bB;
pa的結構就是A的佈局,就是說pa只能訪問到bB對象的前兩項,訪問不到第三項int b。
pa->g(); //編譯器知道g是一個聲明爲virtual的成員函數,而且其入口地址放在表格(無論是vtableA表還是vtableB表)的第2項,那麼編譯器編譯這條語句的時候就轉換爲call *(pa->vptr)[1]
這一項放的是B::g()的入口地址,就實現了多態。(bB的vptr指向的是B的虛表vtableA)
虛函數調用:在運行時確定,取決於指針指向實例的類型。