多態和虛函數的編譯器
⑴ C++中多態的作用
1、什麼是虛函數和多態
虛函數是在類中被聲明為virtual的成員函數,當編譯器看到通過指針或引用調用此類函數時,對其執行晚綁定,即通過指針(或引用)指向的類的類型信息來決定該函數是哪個類的。通常此類指針或引用都聲明為基類的,它可以指向基類或派生類的對象。
多態指同一個方法根據其所屬的不同對象可以有不同的行為(根據自己理解,不知這么說是否嚴謹)。
舉個例子說明虛函數、多態、早綁定和晚綁定:
李氏兩兄妹(哥哥和妹妹)參加姓氏運動會(不同姓氏組隊參加),哥哥男子項目比賽,妹妹參加女子項目比賽,開幕式有一個參賽隊伍代表發言儀式,兄妹倆都想去露露臉,可只能一人去,最終他們決定到時抓鬮決定,而組委會也不反對,它才不關心是哥哥還是妹妹來發言,只要派一個姓李的來說兩句話就行。運動會如期舉行,妹妹抓鬮獲得代表李家發言的機會,哥哥參加了男子項目比賽,妹妹參加了女子項目比賽。比賽結果就不是我們關心的了。
現在讓我們來做個類比(只討論與運動會相關的話題):
(1)類的設計:
李氏兄妹屬於李氏家族,李氏是基類(這里還是抽象的純基類),李氏又派生出兩個子類(李氏男和李氏女),李氏男會所有男子項目的比賽(李氏男的成員函數),李氏女會所有女子項目的比賽(李氏女的成員函數)。姓李的人都會發言(基類虛函數),李氏男和李氏女繼承自李氏當然也會發言,只是男女說話聲音不一樣,內容也會又差異,給人感覺不同(李氏男和李氏女分別重新定義發言這個虛函數)。李氏兩兄妹就是李氏男和李氏女兩個類的實體。
(2)程序設計:
李氏兄妹填寫參賽報名表。
(3)編譯:
李氏兄妹的參賽報名表被上交給組委會(編譯器),哥哥和妹妹分別參加男子和女子的比賽,組委會一看就明白了(早綁定),只是發言人選不明確,組委會看到報名表上寫的是「李家代表」(基類指針),組委會不能確定到底是誰,就做了個備註:如果是男的,就是哥哥李某某;如果是女的,就是妹妹李某某(晚綁定)。組委會做好其它准備工作後,就等運動會開始了(編譯完畢)。
(4)程序運行:
運動會開始了(程序開始運行),開幕式上我們聽到了李家妹妹的發言,如果是哥哥運氣好抓鬮勝出,我們將聽到哥哥的發言(多態)。然後就是看到兄妹倆參加比賽了。。。
但願這個比喻說清楚了虛函數、多態、早綁定和晚綁定的概念和它們之間的關系。再說一下,早綁定指編譯器在編譯期間即知道對象的具體類型並確定此對象調用成員函數的確切地址;而晚綁定是根據指針所指對象的類型信息得到類的虛函數表指針進而確定調用成員函數的確切地址。
2、揭密晚綁定的秘密
編譯器到底做了什麼實現的虛函數的晚綁定呢?我們來探個究竟。
編譯器對每個包含虛函數的類創建一個表(稱為V TA B L E)。在V TA B L E中,編譯器放置特定類的虛函數地址。在每個帶有虛函數的類中,編譯器秘密地置一指針,稱為v p o i n t e r(縮寫為V P T R),指向這個對象的V TA B L E。通過基類指針做虛函數調用時(也就是做多態調用時),編譯器靜態地插入取得這個V P T R,並在V TA B L E表中查找函數地址的代碼,這樣就能調用正確的函數使晚捆綁發生。為每個類設置V TA B L E、初始化V P T R、為虛函數調用插入代碼,所有這些都是自動發生的,所以我們不必擔心這些。利用虛函數,這個對象的合適的函數就能被調用,哪怕在編譯器還不知道這個對象的特定類型的情況下。(《C++編程思想》)
在任何類中不存在顯示的類型信息,可對象中必須存放類信息,否則類型不可能在運行時建立。那這個類信息是什麼呢?我們來看下面幾個類:
class no_virtual
{
public:
void fun1() const{}
int fun2() const { return a; }
private:
int a;
}
class one_virtual
{
public:
virtual void fun1() const{}
int fun2() const { return a; }
private:
int a;
}
class two_virtual
{
public:
virtual void fun1() const{}
virtual int fun2() const { return a; }
private:
int a;
}
以上三個類中:
no_virtual沒有虛函數,sizeof(no_virtual)=4,類no_virtual的長度就是其成員變數整型a的長度;
one_virtual有一個虛函數,sizeof(one_virtual)=8;
two_virtual有兩個虛函數,sizeof(two_virtual)=8; 有一個虛函數和兩個虛函數的類的長度沒有區別,其實它們的長度就是no_virtual的長度加一個void指針的長度,它反映出,如果有一個或多個虛函數,編譯器在這個結構中插入一個指針( V P T R)。在one_virtual 和two_virtual之間沒有區別。這是因為V P T R指向一個存放地址的表,只需要一個指針,因為所有虛函數地址都包含在這個表中。
這個VPTR就可以看作類的類型信息。
那我們來看看編譯器是怎麼建立VPTR指向的這個虛函數表的。先看下面兩個類:
class base
{
public:
void bfun(){}
virtual void vfun1(){}
virtual int vfun2(){}
private:
int a;
}
class derived : public base
{
public:
void dfun(){}
virtual void vfun1(){}
virtual int vfun3(){}
private:
int b;
}
兩個類VPTR指向的虛函數表(VTABLE)分別如下:
base類
——————
VPTR——> |&base::vfun1 |
——————
|&base::vfun2 |
——————
derived類
———————
VPTR——> |&derived::vfun1 |
———————
|&base::vfun2 |
———————
|&derived::vfun3 |
———————
每當創建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就為這個類創建一個VTABLE,如上圖所示。在這個表中,編譯器放置了在這個類中或在它的基類中所有已聲明為virtual的函數的地址。如果在這個派生類中沒有對在基類中聲明為virtual的函數進行重新定義,編譯器就使用基類的這個虛函數地址。(在derived的VTABLE中,vfun2的入口就是這種情況。)然後編譯器在這個類中放置VPTR。當使用簡單繼承時,對於每個對象只有一個VPTR。VPTR必須被初始化為指向相應的VTABLE,這在構造函數中發生。
一旦VPTR被初始化為指向相應的VTABLE,對象就"知道"它自己是什麼類型。但只有當虛函數被調用時這種自我認知才有用。
VPTR常常位於對象的開頭,編譯器能很容易地取到VPTR的值,從而確定VTABLE的位置。VPTR總指向VTABLE的開始地址,所有基類和它的子類的虛函數地址(子類自己定義的虛函數除外)在VTABLE中存儲的位置總是相同的,如上面base類和derived類的VTABLE中vfun1和vfun2的地址總是按相同的順序存儲。編譯器知道vfun1位於VPTR處,vfun2位於VPTR+1處,因此在用基類指針調用虛函數時,編譯器首先獲取指針指向對象的類型信息(VPTR),然後就去調用虛函數。如一個base類指針pBase指向了一個derived對象,那pBase->vfun2()被編譯器翻譯為 VPTR+1 的調用,因為虛函數vfun2的地址在VTABLE中位於索引為1的位置上。同理,pBase->vfun3()被編譯器翻譯為 VPTR+2的調用。這就是所謂的晚綁定。
我們來看一下虛函數調用的匯編代碼,以加深理解。
void test(base* pBase)
{
pBase->vfun2();
}
int main(int argc, char* argv[])
{
derived td;
test(&td);
return 0;
}
derived td;編譯生成的匯編代碼如下:
mov DWORD PTR _td$[esp+24], OFFSET FLAT:??_7derived@@6B@ ; derived::`vftable'
由編譯器的注釋可知,此時PTR _td$[esp+24]中存儲的就是derived類的VTABLE地址。
test(&td);編譯生成的匯編代碼如下:
lea eax, DWORD PTR _td$[esp+24]
mov DWORD PTR __$EHRec$[esp+32], 0
push eax
call ?test@@YAXPAVbase@@@Z ; test
調用test函數時完成了如下工作:取對象td的地址,將其壓棧,然後調用test。
pBase->vfun2();編譯生成的匯編代碼如下:
mov ecx, DWORD PTR _pBase$[esp-4]
mov eax, DWORD PTR [ecx]
jmp DWORD PTR [eax+4]
首先從棧中取出pBase指針指向的對象地址賦給ecx,然後取對象開頭的指針變數中的地址賦給eax,此時eax的值即為VPTR的值,也就是VTABLE的地址。最後就是調用虛函數了,由於vfun2位於VTABLE的第二個位置,相當於 VPTR+1,每個函數指針是4個位元組長,所以最後的調用被編譯器翻譯為 jmp DWORD PTR [eax+4]。如果是調用pBase->vfun1(),這句就該被編譯為jmp DWORD PTR [eax]。
⑵ 編譯時多態性使用什麼獲得!A重載函數B繼承C虛函數D.B和C
函數重載和模板。就這題來說選A。
繼承和虛函數對應的多態需要在運行的時候才能確定具體對象,所以不屬於編譯時多態。
函數重載是讓一個函數名對應多個函數,編譯器會根據調用時候的特徵確定要調用的函數,不需要再運行時處理。
而模板是讓一個一個類型模板或者函數模板對應多個類型或者函數,編譯器根據對模板實例化是使用的參數生成具體的類和函數,也不是在運行時進行的。
另外注意模板變數不屬於多態范疇。
⑶ 1. 編譯時的多態性與運行時的多態性有什麼區別,他們的實現方法有什麼不同
多態從實現的角度可以劃為兩類:編譯時多態和運行時多態。
編譯時的多態性:就是在程序編譯的時候,也就是生成解決方案的時候就決定要實現什麼操作。
運行時的多態性:就是指直到系統運行時,才根據實際情況決定實現何種操作。
1、多態實現形式不同:
編譯時的多態是通過靜態連編來實現的;運行時的多態是用動態連編來實現的。
2、多態性通過方式不同:
編譯時的多態性主要是通過函數重載和運算符重載來實現的;運行時的多態性主要是通過虛函數來實現的。
(3)多態和虛函數的編譯器擴展閱讀:
靜態多態性又稱編譯時的多態性。靜態多態性的函數調用速度快、效率高但缺乏靈活性,在程序運行前就應決定執行的函數和方法。
動態多態性的特點是:不在編譯時確定調用的是哪個函數,而是在程序運行過程中才動態地確定操作所針對的對象。又稱運行時的多態性。動態多態性是通過虛函數(virtual function)實現的。
⑷ 對於c++中的多態,同化效應,虛函數該怎樣理解和使用
先給個小例子。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void DoWork()
{
cout << "Base" << endl;
}
};
class DeriveFirst : public Base
{
public:
virtual void DoWork()
{
cout << "DeriveFirst" << endl;
}
};
class DeriveSecond : public Base
{
public:
virtual void DoWork()
{
cout << "DeriveSecond" << endl;
}
};
void Work(Base* pBase)
{
// 這里的pBase表現出了多態,對於pBase的不同實際類型,這里會做不同的事
pBase->DoWork();
}
int main()
{
Base* pFirst = new DeriveFirst();
Work(pFirst);
Base* pSecond = new DeriveSecond();
Work(pSecond);
return 0;
}
多態:在Work函數中的「pBase->DoWork();」這一行代碼就表現出了pBase的多態性,當pBase的實際類型是DeriveSecond時,它調用的是DeriveSecond版本的DoWork,當它是DeriveFirst時,它調用的是DeriveFirst版本的DoWork。
同化效應:老實說之前沒見過這個名字。猜測它所指的應該是在Work中,不需要關心pBase的具體類型,對於合法的以Base為基類的任何類的對象,在這里都能一致的處理它,只需要調用DoWork就能在運行時達到我們想要的效果。
虛函數:這是C++用來實現多態的機制。如果你想了解整個虛函數的實現機制建議你去看《深入理解C++對象模型》這一本書。這里簡單說一下,一般編譯器都是通過虛函數表的方式來實現虛函數,即對於擁有虛函數的類,它內部會存儲一張函數映射表,當你調用某一個函數時,它會根據這個函數對應的索引找到正確的函數版本。
⑸ 虛函數的作用是什麼有哪些用處何處體現多態
虛函數聯繫到多態,多態聯繫到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。
下面是對C++的虛函數這玩意兒的理解。
一, 什麼是虛函數(如果不知道虛函數為何物,但有急切的想知道,那你就應該從這里開始)
簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將介面與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而採用不同的策略。下面來看一段簡單的代碼
class A{
public:
void print(){ cout<<」This is A」<<endl;}
};
class B:public A{
public:
void print(){ cout<<」This is B」<<endl;}
};
int main(){ //為了在以後便於區分,我這段main()代碼叫做main1
A a;
B b;
a.print();
b.print();
}
通過class A和class B的print()這個介面,可以看出這兩個class因個體的差異而採用了不同的策略,輸出的結果也是我們預料中的,分別是This is A和This is B。但這是否真正做到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操作對象。那現在就把main()處的代碼改一改。
int main(){ //main2
A a;
B b;
A* p1=&a;
A* p2=&b;
p1->print();
p2->print();
}
運行一下看看結果,喲呵,驀然回首,結果卻是兩個This is A。問題來了,p2明明指向的是class B的對象但卻是調用的class A的print()函數,這不是我們所期望的結果,那麼解決這個問題就需要用到虛函數
class A{
public:
virtual void print(){ cout<<」This is A」<<endl;} //現在成了虛函數了
};
class B:public A{
public:
void print(){ cout<<」This is B」<<endl;} //這里需要在前面加上關鍵字virtual嗎?
};
毫無疑問,class A的成員函數print()已經成了虛函數,那麼class B的print()成了虛函數了嗎?回答是Yes,我們只需在把基類的成員函數設為virtual,其派生類的相應的函數也會自動變為虛函數。所以,class B的print()也成了虛函數。那麼對於在派生類的相應函數前是否需要用virtual關鍵字修飾,那就是你自己的問題了。
現在重新運行main2的代碼,這樣輸出的結果就是This is A和This is B了。
現在來消化一下,我作個簡單的總結,指向基類的指針在操作它的多態類對象時,會根據不同的類對象,調用其相應的函數,這個函數就是虛函數。
二, 虛函數是如何做到的(如果你沒有看過《Inside The C++ Object Model》這本書,但又急切想知道,那你就應該從這里開始)
虛函數是如何做到因對象的不同而調用其相應的函數的呢?現在我們就來剖析虛函數。我們先定義兩個類
class A{ //虛函數示例代碼
public:
virtual void fun(){cout<<1<<endl;}
virtual void fun2(){cout<<2<<endl;}
};
class B:public A{
public:
void fun(){cout<<3<<endl;}
void fun2(){cout<<4<<endl;}
};
由於這兩個類中有虛函數存在,所以編譯器就會為他們兩個分別插入一段你不知道的數據,並為他們分別創建一個表。那段數據叫做vptr指針,指向那個表。那個表叫做vtbl,每個類都有自己的vtbl,vtbl的作用就是保存自己類中虛函數的地址,我們可以把vtbl形象地看成一個數組,這個數組的每個元素存放的就是虛函數的地址,請看圖
通過上圖,可以看到這兩個vtbl分別為class A和class B服務。現在有了這個模型之後,我們來分析下面的代碼
A *p=new A;
p->fun();
毫無疑問,調用了A::fun(),但是A::fun()是如何被調用的呢?它像普通函數那樣直接跳轉到函數的代碼處嗎?No,其實是這樣的,首先是取出vptr的值,這個值就是vtbl的地址,再根據這個值來到vtbl這里,由於調用的函數A::fun()是第一個虛函數,所以取出vtbl第一個slot里的值,這個值就是A::fun()的地址了,最後調用這個函數。現在我們可以看出來了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里裝著對應類的虛函數地址,所以這樣虛函數就可以完成它的任務。
而對於class A和class B來說,他們的vptr指針存放在何處呢?其實這個指針就放在他們各自的實例對象里。由於class A和class B都沒有數據成員,所以他們的實例對象里就只有一個vptr指針。通過上面的分析,現在我們來實作一段代碼,來描述這個帶有虛函數的類的簡單模型。
#include<iostream>
using namespace std;
//將上面「虛函數示例代碼」添加在這里
int main(){
void (*fun)(A*);
A *p=new B;
long lVptrAddr;
memcpy(&lVptrAddr,p,4);
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);
fun(p);
delete p;
system("pause");
}
用VC或Dev-C++編譯運行一下,看看結果是不是輸出3,如果不是,那麼太陽明天肯定是從西邊出來。現在一步一步開始分析
void (*fun)(A*); 這段定義了一個函數指針名字叫做fun,而且有一個A*類型的參數,這個函數指針待會兒用來保存從vtbl里取出的函數地址
A* p=new B; new B是向內存(內存分5個區:全局名字空間,自由存儲區,寄存器,代碼空間,棧)自由存儲區申請一個內存單元的地址然後隱式地保存在一個指針中.然後把這個地址附值給A類型的指針P.
.
long lVptrAddr; 這個long類型的變數待會兒用來保存vptr的值
memcpy(&lVptrAddr,p,4); 前面說了,他們的實例對象里只有vptr指針,所以我們就放心大膽地把p所指的4bytes內存里的東西復制到lVptrAddr中,所以復制出來的4bytes內容就是vptr的值,即vtbl的地址
現在有了vtbl的地址了,那麼我們現在就取出vtbl第一個slot里的內容
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); 取出vtbl第一個slot里的內容,並存放在函數指針fun里。需要注意的是lVptrAddr裡面是vtbl的地址,但lVptrAddr不是指針,所以我們要把它先轉變成指針類型
fun(p); 這里就調用了剛才取出的函數地址里的函數,也就是調用了B::fun()這個函數,也許你發現了為什麼會有參數p,其實類成員函數調用時,會有個this指針,這個p就是那個this指針,只是在一般的調用中編譯器自動幫你處理了而已,而在這里則需要自己處理。
delete p;和system("pause"); 這個我不太了解,算了,不解釋這個了
如果調用B::fun2()怎麼辦?那就取出vtbl的第二個slot里的值就行了
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 為什麼是加4呢?因為一個指針的長度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 這更符合數組的用法,因為lVptrAddr被轉成了long*型別,所以+1就是往後移sizeof(long)的長度
三, 以一段代碼開始
#include<iostream>
using namespace std;
class A{ //虛函數示例代碼2
public:
virtual void fun(){ cout<<"A::fun"<<endl;}
virtual void fun2(){cout<<"A::fun2"<<endl;}
};
class B:public A{
public:
void fun(){ cout<<"B::fun"<<endl;}
void fun2(){ cout<<"B::fun2"<<endl;}
}; //end//虛函數示例代碼2
int main(){
void (A::*fun)(); //定義一個函數指針
A *p=new B;
fun=&A::fun;
(p->*fun)();
fun = &A::fun2;
(p->*fun)();
delete p;
system("pause");
}
你能估算出輸出結果嗎?如果你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是B::fun和B::fun2,如果你想不通就接著往下看。給個提示,&A::fun和&A::fun2是真正獲得了虛函數的地址嗎?
首先我們回到第二部分,通過段實作代碼,得到一個「通用」的獲得虛函數地址的方法
#include<iostream>
using namespace std;
//將上面「虛函數示例代碼2」添加在這里
void CallVirtualFun(void* pThis,int index=0){
void (*funptr)(void*);
long lVptrAddr;
memcpy(&lVptrAddr,pThis,4);
memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);
funptr(pThis); //調用
}
int main(){
A* p=new B;
CallVirtualFun(p); //調用虛函數p->fun()
CallVirtualFun(p,1);//調用虛函數p->fun2()
system("pause");
}
現在我們擁有一個「通用」的CallVirtualFun方法。
這個通用方法和第三部分開始處的代碼有何聯系呢?聯系很大。由於A::fun()和A::fun2()是虛函數,所以&A::fun和&A::fun2獲得的不是函數的地址,而是一段間接獲得虛函數地址的一段代碼的地址,我們形象地把這段代碼看作那段CallVirtualFun。編譯器在編譯時,會提供類似於CallVirtualFun這樣的代碼,當你調用虛函數時,其實就是先調用的那段類似CallVirtualFun的代碼,通過這段代碼,獲得虛函數地址後,最後調用虛函數,這樣就真正保證了多態性。同時大家都說虛函數的效率低,其原因就是,在調用虛函數之前,還調用了獲得虛函數地址的代碼。
最後的說明:本文的代碼可以用VC6和Dev-C++4.9.8.0通過編譯,且運行無問題。其他的編譯器小弟不敢保證。其中,裡面的類比方法只能看成模型,因為不同的編譯器的低層實現是不同的。例如this指針,Dev-C++的gcc就是通過壓棧,當作參數傳遞,而VC的編譯器則通過取出地址保存在ecx中。所以這些類比方法不能當作具體實現
⑹ 在c++中虛函數和多態性是什麼意思
虛函數是在基類中定義的,目的是不確定它的派生類的具體行為。例:
定義一個基類:class
Animal//動物。它的函數為breathe()//呼吸。
再定義一個類class
Fish//魚
。它的函數也為breathe()
再定義一個類class
Sheep
//羊。它的函數也為breathe()
為了簡化代碼,將Fish,Sheep定義成基類Animal的派生類。
然而Fish與Sheep的breathe不一樣,一個是在水中通過水來呼吸,一個是直接呼吸空氣。所以基類不能確定該如何定義breathe,所以在基類中只定義了一個virtual
breathe,它是一個空的虛函數。具本的函數在子類中分別定義。程序一般運行時,找到類,如果它有基類,再找它的基類,最後運行的是基類中的函數,這時,它在基類中找到的是virtual標識的函數,它就會再回到子類中找同名函數。派生類也叫子類。基類也叫父類。這就是虛函數的產生,和類的多態性(breathe)的體現.
這里的多態性是指類的多態性。
函數的多態性是指一個函數被定義成多個不同參數的函數,它們一般被存在頭文件中,當你調用這個函數,針對不同的參數,就會調用不同的同名函數。例:Rect()//矩形。它的參數可以是兩個坐標點(point,point)也可能是四個坐標(x1,y1,x2,y2)這叫函數的多態性與函數的重載。
⑺ C++中多態(虛函數)是如何實現的
實現虛函數需要對象附帶一些額外信息,以使對象在運行時可以確定該調用哪個虛函數。對大多數編譯器來說,這個額外信息的具體形式是一個稱為vptr(虛函數表指針)的指針。vptr指向的是一個稱為vtbl(虛函數表)的函數指針數組。每個有虛函數的類都附帶有一個vtbl。當對一個對象的某個虛函數進行請求調用時,實際被調用的函數是根據指向vtbl的vptr在vtbl里找到相應的函數指針來確定的
⑻ 虛函數調用為什麼不能在編譯時確定
在編譯的時候編譯器並不知道用戶選擇的是哪種類型的對象。,如果不是虛函數,則採用早綁定,函數體與函數調用在程序運行之前就綁定了.當函數聲明為虛函數時,,編譯器通過創建一個虛函數表存放虛函數的地址,在運行時,通過基類指針做虛函數調用時,編譯器靜態的插入能取得這個虛函數指針並在虛函數表中找到正確的函數版本.