一、虛函數
1. 概念
多態指當不同的對象收到相同的消息時,產生不同的動作
- 編譯時多態(靜態綁定),函數重載,運算符重載,模板。
- 運行時多態(動態綁定),虛函數機制。
為了實現C++的多態,C++使用了一種動態綁定的技術。這個技術的核心是虛函數表(下文簡稱虛表)。本文介紹虛函數表是如何實現動態綁定的。
C++多態實現的原理:
- ?當類中聲明虛函數時,編譯器會在類中生成一個虛函數表
- 虛函數表是一個存儲成員函數地址的數據結構
- 虛函數表是由編譯器自動生成與維護的
- ?virtual成員函數會被編譯器放入虛函數表中
- 存在虛函數表時,每個對象中都有一個指向虛函數表的指針
?
2. 類的虛表
每個包含了虛函數的類都包含一個虛表。?
我們知道,當一個類(A)繼承另一個類(B)時,類A會繼承類B的函數的調用權。所以如果一個基類包含了虛函數,那么其繼承類也可調用這些虛函數,換句話說,一個類繼承了包含虛函數的基類,那么這個類也擁有自己的虛表。
我們來看以下的代碼。類A包含虛函數vfunc1,vfunc2,由于類A包含虛函數,故類A擁有一個虛表。
class A {
public:virtual void vfunc1();virtual void vfunc2();void func1();void func2();
private:int m_data1, m_data2;
};
虛表是一個指針數組,其元素是虛函數的指針,每個元素對應一個虛函數的函數指針。需要指出的是,普通的函數即非虛函數,其調用并不需要經過虛表,所以虛表的元素并不包括普通函數的函數指針。?
虛表內的條目,即虛函數指針的賦值發生在編譯器的編譯階段,也就是說在代碼的編譯階段,虛表就可以構造出來了。
?
3. 虛表指針
虛表是屬于類的,而不是屬于某個具體的對象,一個類只需要一個虛表即可。同一個類的所有對象都使用同一個虛表。?
為了指定對象的虛表,每個對象的內部包含一個虛表的指針,來指向自己所使用的虛表。為了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在類中添加了一個指針,*__vptr,用來指向虛表。這樣,當類的對象在創建時便擁有了這個指針,且這個指針的值會自動被設置為指向類的虛表。
上面指出,一個繼承類的基類如果包含虛函數,那個這個繼承類也有擁有自己的虛表,故這個繼承類的對象也包含一個虛表指針,用來指向它的虛
4. 動態綁定
class A
{
public:virtual void vfunc1();virtual void vfunc2();void func1();void func2();
private:int m_data1, m_data2;
};class B : public A
{
public:virtual void vfunc1();void func1();
private:int m_data3;
};class C: public B
{
public:virtual void vfunc2();void func2();
private:int m_data1, m_data4;
};
類A是基類,類B繼承類A,類C又繼承類B。類A,類B,類C,其對象模型如下圖3所示。
由于這三個類都有虛函數,故編譯器為每個類都創建了一個虛表,即類A的虛表(A vtbl),類B的虛表(B vtbl),類C的虛表(C vtbl)。類A,類B,類C的對象都擁有一個虛表指針,*__vptr,用來指向自己所屬類的虛表。?
- 類A包括兩個虛函數,故A vtbl包含兩個指針,分別指向A::vfunc1()和A::vfunc2()。?
- 類B繼承于類A,故類B可以調用類A的函數,但由于類B重寫了B::vfunc1()函數,故B vtbl的兩個指針分別指向B::vfunc1()和A::vfunc2()。?
- 類C繼承于類B,故類C可以調用類B的函數,但由于類C重寫了C::vfunc2()函數,故C vtbl的兩個指針分別指向B::vfunc1()(指向繼承的最近的一個類的函數)和C::vfunc2()。?
雖然圖3看起來有點復雜,但是只要抓住“對象的虛表指針用來指向自己所屬類的虛表,虛表中的指針會指向其繼承的最近的一個類的虛函數”這個特點,便可以快速將這幾個類的對象模型在自己的腦海中描繪出來。[非虛函數的調用不用經過虛表,故不需要虛表中的指針指向這些函數。
【注意】非虛函數的調用不用經過虛表,故不需要虛表中的指針指向這些函數。
下面以代碼說明,代碼如下:
#include <iostream>
using namespace std;class Base {
public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }
};typedef void(*Fun)(void); //函數指針
int main()
{Base b;// 這里指針操作比較混亂,在此稍微解析下:// *****printf("虛表地址:%p\n", *(int *)&b); 解析*****:// 1.&b代表對象b的起始地址// 2.(int *)&b 強轉成int *類型,為了后面取b對象的前四個字節,前四個字節是虛表指針// 3.*(int *)&b 取前四個字節,即vptr虛表地址//// *****printf("第一個虛函數地址:%p\n", *(int *)*(int *)&b);*****:// 根據上面的解析我們知道*(int *)&b是vptr,即虛表指針.并且虛表是存放虛函數指針的// 所以虛表中每個元素(虛函數指針)在32位編譯器下是4個字節,因此(int *)*(int *)&b// 這樣強轉后為了后面的取四個字節.所以*(int *)*(int *)&b就是虛表的第一個元素.// 即f()的地址.// 那么接下來的取第二個虛函數地址也就依次類推. 始終記著vptr指向的是一塊內存,// 這塊內存存放著虛函數地址,這塊內存就是我們所說的虛表.//printf("虛表地址:%p\n", *(int *)&b);printf("第一個虛函數地址:%p\n", *(int *)*(int *)&b);printf("第二個虛函數地址:%p\n", *((int *)*(int *)(&b) + 1));Fun pfun = (Fun)*((int *)*(int *)(&b)); //vitural f();printf("f():%p\n", pfun);pfun();pfun = (Fun)(*((int *)*(int *)(&b) + 1)); //vitural g();printf("g():%p\n", pfun);pfun();
}
輸出結果:
通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛函數表的地址,然后,再次取址就可以得到第一個虛函數的地址了,也就是Base::f(),這在上面的程序中得到了驗證(把int* 強制轉成了函數指針)。通過這個示例,我們就可以知道如果要調用Base::g()和Base::h(),其代碼如下
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
?
二、一般繼承?
下面,再讓我們來看看繼承時的虛函數表是什么樣的。假設有如下所示的一個繼承關系:
class Base
{
public:virtual void f() { cout << "Base::f()" << endl; }virtual void g() { cout << "Base::g()" << endl; }virtual void h() { cout << "Base::h()" << endl; }
};class Derive : public Base
{
public:virtual void f1() { cout << "Base::f1()" << endl; }virtual void g1() { cout << "Base::g1()" << endl; }virtual void h1() { cout << "Base::h1()" << endl; }
};
?
?請注意,在這個繼承關系中,子類沒有重載任何父類的函數。那么,在派生類的實例中,其虛函數表如下所示:
對于實例:Derive d; 的虛函數表如下:
我們可以看到下面幾點:
- 虛函數按照其聲明順序放于表中。
- 父類的虛函數在子類的虛函數前面。
?
三、一般繼承(有虛函數覆蓋)?
覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。
class Base
{
public:virtual void f() { cout << "Base::f()" << endl; }virtual void g() { cout << "Base::g()" << endl; }virtual void h() { cout << "Base::h()" << endl; }
};class Derive : public Base
{
public:virtual void f() { cout << "Base::f1()" << endl; }virtual void g1() { cout << "Base::g1()" << endl; }virtual void h1() { cout << "Base::h1()" << endl; }
};
為了讓大家看到被繼承過后的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()。那么,對于派生類的實例,其虛函數表會是下面的一個樣子:
我們從表中可以看到下面幾點,
- 覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
- 沒有被覆蓋的函數依舊。
這樣,我們就可以看到對于下面這樣的程序:
Base *b = new Derive();
b->f();
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,于是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
?
四、單一的一般繼承
?下面,我們假設有如下所示的一個繼承關系:
注意,在這個繼承關系中,父類,子類,孫子類都有自己的一個成員變量。而了類覆蓋了父類的f()方法,孫子類覆蓋了子類的g_child()及其超類的f()。
測試代碼:
#include<iostream>
using namespace std;class Parent {
public:int iparent;Parent(): iparent(10) {}virtual void f() { cout << " Parent::f()" << endl; }virtual void g() { cout << " Parent::g()" << endl; }virtual void h() { cout << " Parent::h()" << endl; }
};class Child : public Parent {
public:int ichild;Child(): ichild(100) {}virtual void f() { cout << "Child::f()" << endl; }virtual void g_child() { cout << "Child::g_child()" << endl; }virtual void h_child() { cout << "Child::h_child()" << endl; }
};class GrandChild : public Child {
public:int igrandchild;GrandChild(): igrandchild(1000) {}virtual void f() { cout << "GrandChild::f()" << endl; }virtual void g_child() { cout << "GrandChild::g_child()" << endl; }virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};int main()
{typedef void(*Fun)(void);GrandChild gc;int** pVtab = (int**)&gc;cout << "[0] GrandChild::_vptr->" << endl;for (int i = 0; (Fun)pVtab[0][i] != NULL; i++) {Fun pFun = (Fun)pVtab[0][i];cout << " [" << i << "] ";pFun();}cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl;
}
輸出結果:
使用圖片表示如下:
可見以下幾個方面:
- 虛函數表在最前面的位置。
- 成員變量根據其繼承和聲明順序依次放在后面。
- 在單一的繼承中,被overwrite的虛函數在虛函數表中得到了更新。
?
參考資料
- c++中虛基類表和虛函數表的布局
?