虛調用的幾種具體情形
虛調用是相對於實調用而言的,它的本質是動態聯編(後面我們會講到)。
實調用:在發生函數調用的時候,如果函數的地址是在編譯階段確定的,就是實調用。反之,函數的入口地址要在運行時通過查
詢虛函數表的方式獲得,就是虛調用。
虛調用不能簡單理解爲"對虛函數的調用", 因爲對虛函數的調用很可能是實調用。
下面這個程序,對虛函數的調用就是實調用
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } }; void func(A a) { a.show(); //a是A的一個實例,並不是指向類A對象的指針或引用,所以爲實調用。 } int main() { B b; func(b); //調用類A的拷貝構造函數,產生一個A類對象作爲a進入函數func()的函數體 //在函數體內,a是一個純粹的類A 對象,與類型B 毫無關係 return 0; }
在構造函數中調用虛函數,對虛函數的調用實際上是實調用(一般情況下,因避免在構造函數中調用虛函數)。這是虛函數被實調用的另一個例子。怎麼理解呢?從概念上說,在一個對象的構造函數運行完畢之前,這個對象還沒有完全誕生,所以在構造函數中調用虛函數,實際上都是實調用。請看下面的一個例子。在構造函數中調用虛函數:
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } A() { show(); //調用虛函數 } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } //B() //{ // show(); // //} }; int main() { A a; B b; return 0; }
現在,我們來看一下虛函數到底是幹什麼用的?設立虛函數的初衷,就是想在設計基類的時候,對該基類的派生類實施一定程度的控制。可以理解爲“通過基類訪問派生類成
員”。因此,虛調用最常用的形式是:通過指向基類對象的指針訪問派生類對象的虛函數,或通過基類對象的引用調用派生類
對象的虛函數。虛調用是通過查詢虛函數表來實現的,而擁有虛函數的對象都可以訪問到所屬類的虛函數表。
派生類對象怎麼訪問到基類對象的虛函數?
通過指向派生類對象的指針或引用調用基類對象的虛函數,下面就是一個具體例子:
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } }; int main() { A a; //通過派生類對象的引用pb 實現了調用基類中虛函數show(),, //如果把 A中show() 前面的virtual去掉, 則調用的就是B 中的show() B &pb = static_cast<B&>(a); pb.show(); //調用的是基類 A的 show(); return 0; }
是不是實現虛調用一定要顯式藉助於指針或引用才能實現呢?當然不是,請看下面的例子:
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } void callfunc() { show(); } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } }; int main() { B b; b.callfunc(); //調用的是A::callfunc(),,但在A::callfunc()調用的是B::show() //這就是一個虛調用 A a; a.callfunc(); //這裏調用的是A::show() return 0; }
虛函數可以是私有的嗎?
虛函數一般被聲明爲公有的,這樣實現虛函數的調用會比較方便。但C++並沒有要求虛函數必須是公有的,將虛函數設置成私
有的和受保護並不妨礙虛函數之間的覆蓋和虛函數的調用。
動態聯編怎麼實現?
動態聯編:是指被調函數的入口地址是在運行時、而不是在編譯時決定的。C++利用動態聯編來完成虛函數的調用,C++標準
並沒有規定如何實現動態聯編,但大多數的C++編譯器都是通過虛指針(vptr)和虛函數表(vtable)來實現動態聯編的。
基本思路:
1.爲每一個包含虛函數的類建立一個虛函數表,虛函數表的各個表項存放的是各虛函數在內存中的入口地址;
2.在該類的每一個對象中設置一個指向虛函數表的指針(這就是爲什麼含有虛函數的對象會多出4個字節的大小);
3.在調用虛函數的時候,先利用虛指針找到虛函數表,確定虛函數的入口地址在表中的位置,獲取入口地址完成調用。
下面來詳細瞭解一下虛指針和虛函數表:
(1) 虛指針(vptr)放在對象的哪個位置?
虛指針是作爲對象的一部分存放在對象的空間中的,一個類只有一個虛函數表,因此該類的所有對象的虛指針都指向同一個地
方。在不同的編譯器中,虛指針在對象中的位置是不同的,在Vistual C++中,虛指針位於對象的其實位置,在GUN C++中,
虛指針位於對象的尾部而不是頭部。那麼怎麼確定虛指針到底存放在哪呢,看下面的程序:
#include <iostream> using namespace std; class HaveVirtual { int i; public: HaveVirtual() { i = 1; } virtual void show() { cout<<"you are hear\n"; } }; int main() { HaveVirtual hv; unsigned long *p; p = reinterpret_cast<unsigned long*>(&hv); cout<<p[0]<<endl; cout<<p[1]<<endl; return 0; }
通過觀察p[0] 和 p[1]的值,就可以判斷虛指針放在哪了。
(2)虛函數表的內部結構
一個類只有一個虛函數表,所有的類都不會和其它的類共享同一張虛函數表。
怎麼創建虛函數表呢?
1.確定當前類包含的虛函數的個數。一個類的虛函數有兩個來源:一是繼承自父類(可能在當前類中改寫),其它的是在當前類
中新聲明的虛函數;
2.爲所有虛函數排序。繼承自父類的所有虛函數,排在當前類新聲明的虛函數之前,新聲明的虛函數按照在當前類中聲明的順
序排列;
3.確定虛函數的入口地址。繼承自父類的虛函數,如果在當前類中被改寫,則虛函數的入口地址是改寫之後的函數的地址,否
則保留父類中的虛函數的入口地址。新聲明的虛函數的入口地址就是在當前類中的函數的入口地址。
(3)虛函數表放在哪裏
虛函數表放在應用程序的常量區。虛函數的每一項代表了一個函數的入口地址,類型是Double Word。
(4)通過訪問虛函數表手動調用虛函數
既然知道了虛函數表的位置和結構,那麼就可以通過訪問虛函數表,手動調用虛函數。
下面是一個手動調用虛函數的例子:
#include <iostream> using namespace std; typedef void (*funptr)(); //定義一個函數指針funptr void ExecuteVirtualFunc(void * pObj, int index) { funptr p; unsigned long * pAddr; pAddr = reinterpret_cast<unsigned long*>(pObj); //取得對象的虛指針 //visual C++中虛指針放在對象的頭部 pAddr = (unsigned long *)*pAddr; //通過虛指針得到虛函數表的首地址 p = (funptr)pAddr[index]; //通過索引獲得虛函數入口地址 _asm { mov ecx, pObj //將對象的首地址放入寄存器 ecx } p(); //調用函數 } class Base { int i; public: Base() { i = 0; } virtual void f1() { cout<<"Base's f1()\n"; } virtual void f2() { cout<<"Base's f2()\n"; } virtual void f3() { cout<<"Base's f3()\n"; } }; class Derived:public Base { int j; public: Derived() { j = 2; } virtual void f4() { cout<<"Derived's f4()\n"; } void f3() { cout<<"Derived's f3()\n"; } void f1() { cout<<"Derived's f1()\n"; } }; int main() { Base b; Derived d; ExecuteVirtualFunc(&b, 1); //調用對象b 的第2個虛函數 f2() ExecuteVirtualFunc(&d, 3); //調用對象d 的第4個虛函數 f4() return 0; }
調用類的非靜態成員函數是,必須同時給出對象的首地址,所以在程序中使用內聯彙編代碼_asm { mov ecx, pObj ecx }來達到這個目的。在Visual C++中,在調用類的非靜態成員函數之前,對象的首地址都是送往寄存器 ecx 的。