一.什么是函數(shù)指針?
函數(shù)指針,顧名思義即指向函數(shù)的指針。
void print() { } void (*pfun)(); //聲明一個(gè)指向函數(shù)的指針,函數(shù)的參數(shù)是 void,函數(shù)的返回值是 void pfun = print; //賦值一個(gè)指向函數(shù)的指針 (*pfun)(); //使用一個(gè)指向函數(shù)的指針
比較簡(jiǎn)單,不是嗎?為什么*pfun需要用()擴(kuò)起來(lái)呢?
因?yàn)?的運(yùn)算符優(yōu)先級(jí)比()低,如果不用()就成了*(pfun()).
指向類(lèi)的成員函數(shù)的指針不過(guò)多了一個(gè)類(lèi)的限定而已!
class A { void speak(char *, const char *); }; void main() { A a; void (A::*pmf)(char *, const char *);//指針的聲明 pmf = &A::speak; //指針的賦值 }
一個(gè)指向類(lèi)A 成員函數(shù)的指針聲明為:
void (A::*pmf)(char *, const char *);
聲明的解釋是:pmf是一個(gè)指向A成員函數(shù)的指針,返回?zé)o類(lèi)型值,函數(shù)帶有二個(gè)參數(shù),參數(shù)的類(lèi)型分別是char *和const char *。除了在星號(hào)前增加A::,與聲明外部函數(shù)指針的方法一樣。一種更加高明的方法是使用類(lèi)型定義:例如,下面的語(yǔ)句定義了PMA是一個(gè)指向類(lèi)A成成員函數(shù)的指針,函數(shù)返回?zé)o類(lèi)型值,函數(shù)參數(shù)類(lèi)型為char *和const char *:
typedef void(A::*PMA)(char *,const char *);
PMA pmf= &A::strcat;//pmf是 PMF類(lèi)型(類(lèi)A成員指針)的變量
如果要問(wèn),為什么能用一個(gè)指針指向一個(gè)函數(shù)呢?我覺(jué)得要理解這個(gè)問(wèn)題,以及要理解后面的函數(shù)指針和類(lèi)成員函數(shù)指針,沒(méi)有什么比從計(jì)算機(jī)原理的角度來(lái)理解更容易了。這里就簡(jiǎn)要回顧一下相關(guān)知識(shí)。
眾所周知,計(jì)算機(jī)(圖靈機(jī))執(zhí)行程序的基本流程就是:取指令->執(zhí)行指令->取下一條指令->……。取指令的位置由一個(gè)寄存器PC決定。開(kāi)機(jī)時(shí),PC通常指向0(這條指令常常是一條跳轉(zhuǎn)指令。在A(yíng)RM架構(gòu)中,一般位置0是復(fù)位中斷向量。總之都是要實(shí)現(xiàn)一個(gè)跳轉(zhuǎn)),隨后,如果沒(méi)有碰到跳轉(zhuǎn)指令,則PC自加一個(gè)字長(zhǎng)執(zhí)行下一條指令,否則根據(jù)跳轉(zhuǎn)指令(JB,JMP,CALL等)跳轉(zhuǎn)到給定位置(即改寫(xiě)PC)執(zhí)行。
常見(jiàn)的C語(yǔ)言程序中,if、while、for等帶判斷條件的指令是由條件跳轉(zhuǎn)語(yǔ)句完成(ARM架構(gòu)下指令為B,原理都是一樣的)。以一個(gè)簡(jiǎn)單的條件跳轉(zhuǎn)指令舉例:
if ( a == 0 )
00CD52D5 cmp dword ptr [a],0 ;比較
00CD52D9 jne main+34h (0CD52E4h) ;根據(jù)結(jié)果跳轉(zhuǎn)到0CD52E4h(if語(yǔ)句塊后)
goto、break、while語(yǔ)句則一般是jmp無(wú)條件跳轉(zhuǎn)指令實(shí)現(xiàn)的。如以下程序:
while ( a!=0 )
008E52DF cmp dword ptr [a],0
008E52E3 je main+40h (08E52F0h)
{
}
008E52E5 jmp main+2Fh (08E52DFh)
可以看出來(lái),跳轉(zhuǎn)指令,無(wú)論是有無(wú)條件,無(wú)論尋址方式如何,其實(shí)都只是做了一件事——改變PC的值,跳轉(zhuǎn)到指定位置。這個(gè)位置是由編譯器給定的值,在此過(guò)程中,不會(huì)做除了改變PC以外的任何事情。
CALL指令相對(duì)于跳轉(zhuǎn)指令則不同。從設(shè)計(jì)目的的角度來(lái)說(shuō),CALL是為了實(shí)現(xiàn)函數(shù)調(diào)用。函數(shù)調(diào)用與一般的跳轉(zhuǎn)相比,除了改變PC還需要考慮調(diào)用結(jié)束后恢復(fù)PC值返回原位置。高級(jí)語(yǔ)言中,都是使用堆棧來(lái)處理這問(wèn)題:調(diào)用函數(shù)時(shí),將返回的位置和傳遞參數(shù)壓棧后跳轉(zhuǎn);調(diào)用結(jié)束時(shí),彈出參數(shù)和返回位置隨后跳回?,F(xiàn)代處理器中,CALL指令除了跳轉(zhuǎn),還負(fù)責(zé)將返回位置壓棧,相應(yīng)的RET語(yǔ)句則集成了彈出返回位置和跳轉(zhuǎn)。
CALL指令的尋址方式也有多種(跳轉(zhuǎn)語(yǔ)句同理)。概括性地說(shuō),有三種方式:相對(duì)轉(zhuǎn)移(給PC加減一定值)、絕對(duì)轉(zhuǎn)移(直接給PC賦值)、間接轉(zhuǎn)移(將寄存器中的地址賦值給PC)。順便一說(shuō),在RISC處理器中,往往只有最后一種。
相對(duì)轉(zhuǎn)移、絕對(duì)轉(zhuǎn)移的目標(biāo)地址,都是很簡(jiǎn)單地直接在指令當(dāng)中給出。換句話(huà)說(shuō),跳轉(zhuǎn)地址是在編譯階段就由編譯器給定了。
但是,有的時(shí)候我們需要在運(yùn)行時(shí)動(dòng)態(tài)地改變跳轉(zhuǎn)地址。而相對(duì)轉(zhuǎn)移、絕對(duì)轉(zhuǎn)移是無(wú)法改變跳轉(zhuǎn)地址的。所以,為了達(dá)到這一目的,一般來(lái)說(shuō),是先將跳轉(zhuǎn)地址addr存入一個(gè)寄存器(或內(nèi)存中一個(gè)位置),而后CPU從這個(gè)寄存器中取出地址addr進(jìn)行跳轉(zhuǎn)。這個(gè)地址,也就是我們所說(shuō)的函數(shù)指針。
我們進(jìn)行的各種函數(shù)調(diào)用,本質(zhì)上都是操作這一個(gè)地址,無(wú)論它是固化在指令中還是存儲(chǔ)在寄存器中。而每一個(gè)函數(shù)名,就如同數(shù)組名一樣,實(shí)際上都是一個(gè)地址。所以,理所當(dāng)然地如同一般的指針變量一樣,我們也可以有一個(gè)函數(shù)指針變量。
二.函數(shù)指針的使用
1.一般函數(shù)函數(shù)指針
一般的函數(shù)指針形如:
int( *pf )( char, int, float );
這是一個(gè)有三個(gè)分別為char,int,float輸入?yún)?shù),返回值為int的函數(shù)指針變量pf。為什么*pf需要用()呢?因?yàn)?的運(yùn)算符優(yōu)先級(jí)比()低,如果不用()就成了*(pf())了。
先舉一個(gè)最簡(jiǎn)單的例子:
voidNormalFunc()
{
cout << "Normal Func"<< endl;
}
void( *pfunc )();
pfunc =NormalFunc;
pfunc();//輸出”Normal Func”
稍復(fù)雜的如下
int NormalFunc(char c, int i, float f )
{
cout << "Normal Func"<< c << i << f << endl;
return 0;
}
int( *pfunc)(char,int ,float );
pfunc =NormalFunc;
pfunc(1,2,3);
如果覺(jué)得這樣定義太麻煩,可以用typedef做一個(gè)重命名。如:
typedef int(*I_PFUNC_C_I_F )( char, int, float );//隨手命名,一般情況下別這樣
I_PFUNC_C_I_Fpfunc;
pfunc =NormalFunc;
總之,函數(shù)指針和函數(shù)必須有著完全相同的類(lèi)型(包括參數(shù)、返回值、調(diào)用約定_stdcall和_cdecl)。
這樣有什么好處呢?
【動(dòng)態(tài)調(diào)用、函數(shù)查找表】
二.類(lèi)成員函數(shù)函數(shù)指針
對(duì)于類(lèi)的成員函數(shù),其指針與一般函數(shù)指針有著很大的區(qū)別。主要原因是類(lèi)成員函數(shù)都隱含了一個(gè)this指針,調(diào)用時(shí),編譯器對(duì)其處理方式與一般函數(shù)不同,實(shí)際的匯編代碼也不一樣。因此,為了避免錯(cuò)誤,編譯器是不允許將類(lèi)成員函數(shù)賦值給一般函數(shù)指針的。
實(shí)際的匯編代碼分析見(jiàn)第四節(jié),這里我就只從使用的角度來(lái)解析:如何使用類(lèi)成員函數(shù)指針?
1. 常規(guī)操作方法(推薦)
簡(jiǎn)單來(lái)說(shuō),就是在指針前加入一個(gè)域限定符,并指定成員函數(shù)對(duì)應(yīng)的類(lèi)實(shí)例。
class mc:public empty
{
public:
mc()
{
//構(gòu)造函數(shù)中數(shù)組初始化,必須指定域
maps[0]= &mc::On_WM_PAINT;
maps[1]= &mc::On_WM_DESTORY;
}
typedefLRESULT (_stdcall mc:: *PCFUNC)( HWND, UINT, WPARAM, LPARAM );//類(lèi)成員函數(shù)指針,必須指定域
LRESULT_stdcall On_WM_PAINT(HWND, UINT, WPARAM, LPARAM)
{
cout<< "ON_WM_PAINT" << endl;
return0;
}
LRESULT_stdcall On_WM_DESTORY( HWND, UINT, WPARAM, LPARAM )
{
cout<< "ON_WM_DESTORY" << endl;
return0;
}
PCFUNCmaps[2];//類(lèi)成員函數(shù)指針數(shù)組
voidtest()
{
(this->*maps[1])(NULL, NULL, NULL, NULL );//類(lèi)內(nèi)部調(diào)用
}
};
使用時(shí),在類(lèi)內(nèi)部需要加入this指針,在類(lèi)外部則一定要指定對(duì)應(yīng)的類(lèi)實(shí)例:
mcmyclass;//聲明類(lèi)實(shí)例
mc::PCFUNCpcfunc = myclass.maps[0];//聲明一個(gè)類(lèi)成員函數(shù)指針變量
(myclass.*pcfunc )( NULL, NULL, NULL, NULL );//調(diào)用第0個(gè)函數(shù)輸出“ON_PAINT”
myclass.test();//內(nèi)部調(diào)用,輸出“ON_DESTORY”
2. 靜態(tài)函數(shù)(不推薦)
以上都是對(duì)非靜態(tài)函數(shù)的使用方法。對(duì)于靜態(tài)函數(shù),則要容易得多。因?yàn)殪o態(tài)函數(shù)實(shí)際上就是一個(gè)作用域在類(lèi)內(nèi)部的普通函數(shù),沒(méi)有隱含的this指針。但是相應(yīng)的,靜態(tài)函數(shù)只能使用內(nèi)部的靜態(tài)變量。因此并不推薦。
示例如下:
class mc
{
public:
mc()
{
maps[0]= &mc::On_WM_PAINT;//不需要指定域
maps[1]= &mc::On_WM_DESTORY;
}
typedefLRESULT (_stdcall *PCFUNC)( HWND, UINT, WPARAM, LPARAM );//可見(jiàn)這里不需要指定域
staticint i;
static LRESULT _stdcallOn_WM_PAINT(HWND, UINT, WPARAM, LPARAM)//需要static標(biāo)識(shí)符
{
cout<< "ON_WM_PAINT" << endl;
returni; //static函數(shù)只能使用static變量
}
staticLRESULT _stdcall On_WM_DESTORY( HWND, UINT, WPARAM, LPARAM )
{
cout<< "ON_WM_DESTORY" << endl;
return0;
}
PCFUNCmaps[4];
voidtest()
{
maps[1](NULL, NULL, NULL, NULL );//不需要指定域
}
};
使用時(shí)也不需要指定域:
mc::PCFUNCpcfunc = myclass.maps[0];
myclass.test();
pcfunc(NULL, NULL, NULL, NULL );
3. 友元函數(shù)(較為推薦)
可見(jiàn),靜態(tài)成員函數(shù)的最大限制在于其只能使用static成員變量,這使得其極其難以使用。一個(gè)好的折衷辦法是使用友元函數(shù)。相應(yīng)的,友元函數(shù)能夠操作類(lèi)中的所有成員,不過(guò)不一樣的是必須顯式指定類(lèi)實(shí)例指針。
class mc
{
public:
mc()
{
maps[0]= On_WM_PAINT;//同樣不需要指定域
maps[1]= On_WM_DESTORY;
}
typedefLRESULT( _stdcall *PCFUNC )(mc&,HWND, UINT, WPARAM, LPARAM );//為了能夠操作成員變量,必須傳遞類(lèi)的引用或者指針
staticint i;
friendLRESULT _stdcall On_WM_PAINT(mc&src,HWND, UINT, WPARAM, LPARAM )
{
cout<< "ON_WM_PAINT" << endl;
returnsrc.a;//這樣就能通過(guò)類(lèi)的引用操作成員變量,private限定的也可以
}
friendLRESULT _stdcall On_WM_DESTORY(mc&src, HWND, UINT, WPARAM, LPARAM )
{
cout<< "ON_WM_DESTORY" << endl;
returnsrc.a;
}
PCFUNCmaps[4];
voidtest()
{
maps[1](*this,NULL, NULL, NULL, NULL );//內(nèi)部調(diào)用需要使用this指針
}
private:
int a;
};
在其它地方使用時(shí),也不需要指定域。
mcmyclass;
mc::PCFUNCpcfunc = myclass.maps[0];
myclass.test();
pcfunc(myclass, NULL, NULL, NULL, NULL );
On_WM_PAINT(myclass, NULL, NULL, NULL, NULL );//是的,即使是不用函數(shù)指針,也不需要.或者->,其實(shí)友元函數(shù)就是一個(gè)普通函數(shù)而已。
如果不想在參數(shù)里面加一個(gè)引用,那么可以將這個(gè)類(lèi)的指針?lè)胚M(jìn)一個(gè)全局變量里。如:
mc*mc_handle=nullptr;//全局變量
友元函數(shù)中:
friend LRESULT _stdcall On_WM_PAINT(mc&src, HWND, UINT, WPARAM, LPARAM )
{
cout<< "ON_WM_PAINT" << endl;
returnmc_handle->a;//通過(guò)全局變量操作
}
主函數(shù)中:
mc myclass;
mc_handle=&myclass;//如果不初始化的話(huà),哼哼……
On_WM_PAINT( myclass, NULL, NULL, NULL,NULL );
【如果這個(gè)類(lèi)只會(huì)有一個(gè)實(shí)例,可以使用“單件”設(shè)計(jì)模式】
【如果覺(jué)得用起來(lái)比較麻煩,還可以聲明友元類(lèi)而不是友元函數(shù)】
4. C++11特性:std::function和std::bind(這么高端的操作為什么你不來(lái)試試?)
C++在<functional>中引入了function和bind,使得能夠更加容易地進(jìn)行動(dòng)態(tài)綁定。
簡(jiǎn)單的實(shí)現(xiàn)如下:
class mc
{
public:
mc()
{
fr[0] = bind( &mc::On_WM_PAINT,this,
placeholders::_1,placeholders::_2, placeholders::_3, placeholders::_4 );//綁定方式
}
LRESULT_stdcall On_WM_PAINT( HWND, UINT,WPARAM, LPARAM )
{
cout<< "ON_WM_PAINT" << endl;
returna;
}
LRESULT_stdcall On_WM_DESTORY( HWND, UINT, WPARAM, LPARAM )
{
cout<< "ON_WM_DESTORY" << endl;
returna;
}
function<LRESULT _stdcall( HWND, UINT, WPARAM,LPARAM )>fr[4];//定義方式
};
這樣,調(diào)用時(shí)只需要:
mc myclass;
myclass.fr[0]( NULL, NULL, NULL, NULL );//調(diào)用就是這么簡(jiǎn)單
5.用宏定義來(lái)解放我們(讓讀代碼的人蛋疼去吧)
如果嫌棄上面的這么一大堆東西寫(xiě)起來(lái)太費(fèi)勁,不妨使用下面這種宏定義:
#define ON_WMESSAGE(msgname) LRESULT _stdcall On_##msgname( HWND hwnd,UINTuMsgID,WPARAM wParam,LPARAM lParam )//宏定義
#define MAP_ON_WMESSAGE(msgname) {msgname,On_##msgname}
ON_WMESSAGE( WM_PAINT );
ON_WMESSAGE( WM_DESTROY );
ON_WMESSAGE( WM_LBUTTONDBLCLK );
ON_WMESSAGE( WM_COMMAND );
struct sWM_CB
{
UINTuMsgID;
LRESULT(_stdcall*WndProc )( HWND , UINT , WPARAM , LPARAM );
};
struct sWM_CB WM_CB_Maps[] = {
MAP_ON_WMESSAGE(WM_PAINT ),
MAP_ON_WMESSAGE(WM_DESTROY ),
MAP_ON_WMESSAGE(WM_LBUTTONDBLCLK ),
MAP_ON_WMESSAGE(WM_COMMAND ), };
以上宏定義中的##實(shí)際上是就是將宏定義參數(shù)中的字符直接連接的意思。所以,在這里,ON_WMESSAGE( WM_PAINT )等效語(yǔ)句實(shí)際上是:
LRESULT_stdcall On_WM_PAINT( HWND hwnd,UINT uMsgID,WPARAM wParam,LPARAM lParam )
MAP_ON_WMESSAGE( WM_PAINT )實(shí)際上是:
{ WM_PAINT, On_WM_PAINT }
結(jié)合上以上的function,bind等,實(shí)際上構(gòu)成了C++除了面向?qū)ο?、面向過(guò)程、泛型模板之外的第四種編程——lamada元編程。如果去仔細(xì)探究windows.h源碼,會(huì)發(fā)現(xiàn)這種編程方式到處都是。
#include <iostream> using namespace std; class Person { public: /*這里稍稍注意一下,我將speak()函數(shù)設(shè)置為普通的成員函數(shù),而hello()函數(shù)設(shè)置為虛函數(shù)*/ int value; void speak() { cout << "I am a person!" << endl; printf ("%d\n", &Person::speak); /*在這里驗(yàn)證一下,輸出一下地址就知道了!*/ } virtual void hello() { cout << "Person say \"Hello\"" << endl; } Person() { value = 1; } }; class Baizhantang: public Person { public: void speak() { cout << "I am 白展堂!" << endl; } virtual void hello() { cout << "白展堂 say \"hello!\"" << endl; } Baizhantang() { value = 2; } }; typedef void (Person::*p)();//定義指向Person類(lèi)無(wú)參數(shù)無(wú)返回值的成員函數(shù)的指針 typedef void (Baizhantang::*q)();//定義指向Baizhantang類(lèi)的無(wú)參數(shù)無(wú)返回值的指針 int main() { Person pe; int i = 1; p ip; ip = &Person::speak;//ip指向Person類(lèi)speak函數(shù) (pe.*ip)();//這個(gè)是正確的寫(xiě)法! //-------------------------------------------- //result : I am a Person! // XXXXXXXXXX(表示一段地址) //-------------------------------------------- /* *下面是幾種錯(cuò)誤的寫(xiě)法,要注意! *pe.*ip(); * pe.(*ip)(); *(pe.(*ip))(); */ Baizhantang bzt; q iq = (void (Baizhantang::*)())ip;//強(qiáng)制轉(zhuǎn)換 (bzt.*iq)(); //-------------------------------------------- //result : I am a Person! // XXXXXXXXXX(表示一段地址) //-------------------------------------------- /*有人可能會(huì)問(wèn)了:ip明明被強(qiáng)制轉(zhuǎn)換成了Baizhantang類(lèi)的成員函數(shù)的指針,為什么輸出結(jié)果還是: * I am a Person!在C++里面,類(lèi)的非虛函數(shù)都是采用靜態(tài)綁定,也就是說(shuō)類(lèi)的非虛函數(shù)在編譯前就已經(jīng) *確定了函數(shù)地址!ip之前就是指向Person::speak函數(shù)的地址,強(qiáng)制轉(zhuǎn)換之后,只是指針類(lèi)型變了,里面 *的值并沒(méi)有改變,所以調(diào)用的還是Person.speak函數(shù),細(xì)心的家伙會(huì)發(fā)現(xiàn),輸出的地址都是一致的. *這里要強(qiáng)調(diào)一下:對(duì)于類(lèi)的非靜態(tài)成員函數(shù),c++編譯器會(huì)給每個(gè)函數(shù)的參數(shù)添加上一個(gè)該類(lèi)的指針this,這也 *就是為什么我們?cè)诜庆o態(tài)類(lèi)成員函數(shù)里面可以使用this指針的原因,當(dāng)然,這個(gè)過(guò)程你看不見(jiàn)!而對(duì)于靜態(tài)成員 *函數(shù),編譯器不會(huì)添加這樣一個(gè)this。 */ iq = &Baizhantang::speak;/*iq指向了Baizhantang類(lèi)的speak函數(shù)*/ ip = (void (Person::*)())iq;/*ip接收強(qiáng)制轉(zhuǎn)換之后的iq指針*/ (bzt.*ip)(); //-------------------------------------------- //result : I am 白展堂! //-------------------------------------------- (bzt.*iq)();//這里我強(qiáng)調(diào)一下,使用了動(dòng)態(tài)聯(lián)編,也就是說(shuō)函數(shù)在運(yùn)行是才確定函數(shù)地址! //-------------------------------------------- //result : I am 白展堂! //-------------------------------------------- /*這一部分就沒(méi)有什么好講的了,很明白了!由于speak函數(shù)是普通的成員函數(shù),在編譯時(shí)就知道 *到了Baizhantang::speak的地址,因此(bzt.*ip)()會(huì)輸出“I am 白展堂!”,即使iq被強(qiáng)制轉(zhuǎn)換 *成(void (Person::*)())類(lèi)型的ip,但是其值亦未改變,(bzt.*iq)()依然調(diào)用iq指向處的函數(shù) *即Baizhantang::speak. */ /*好了,上面講完了普通成員函數(shù),我們現(xiàn)在來(lái)玩一點(diǎn)好玩的,現(xiàn)在來(lái)聊虛函數(shù)*/ ip = &Person::hello;/*讓ip指向Person::hello函數(shù)*/ (pe.*ip)(); //-------------------------------------------- //result : Person say "Hello" //-------------------------------------------- (bzt.*ip)(); //-------------------------------------------- //result : 白展堂 say "Hello" //-------------------------------------------- /*咦,這就奇怪了,為何與上面的調(diào)用結(jié)果不類(lèi)似?為什么兩個(gè)調(diào)用結(jié)果不一致?伙伴們注意了: *speak函數(shù)是一個(gè)虛函數(shù),前面說(shuō)過(guò)虛函數(shù)并不是采用靜態(tài)綁定的,而是采用動(dòng)態(tài)綁定,所謂動(dòng)態(tài) *綁定,就是函數(shù)地址得等到運(yùn)行的時(shí)候才確定,對(duì)于有虛函數(shù)的類(lèi),編譯器會(huì)給我們添加一個(gè)指針 *vptr,指向一個(gè)虛函數(shù)表vptl,vptl里面存放著虛函數(shù)的地址,子類(lèi)繼承父類(lèi)的時(shí)候,也會(huì)繼承這樣 *一個(gè)指針,如果子類(lèi)復(fù)寫(xiě)了虛函數(shù),那么該表中該虛函數(shù)地址將會(huì)由父類(lèi)的虛函數(shù)地址替換成子類(lèi)虛 *函數(shù)地址,編譯器會(huì)把(pe.*ip)()轉(zhuǎn)化成為(pe.vptr[1])(pe),加上動(dòng)態(tài)綁定,結(jié)果會(huì)輸出: * Person say "Hello" *(bzt.*ip)()會(huì)被轉(zhuǎn)換成(bzt.vptr[1])(pe),自然會(huì)輸出: *白展堂 say "Hello" *ps:這里我沒(méi)法講得更詳細(xì),因?yàn)榻忉屍饋?lái)肯定是很長(zhǎng)很長(zhǎng)的,感興趣的話(huà),我推薦兩本書(shū)你去看一看: *第一本是侯捷老師的<深入淺出MFC>,里面關(guān)于c++的虛函數(shù)特性講的比較清楚; *第二本是侯捷老師翻譯的<深度探索C++對(duì)象模型>,一聽(tīng)名字就知道,講這個(gè)就更詳細(xì)了; *當(dāng)然,不感興趣的同學(xué)這段解釋可以省略,對(duì)與使用沒(méi)有影響! */ iq = (void (Baizhantang::*)())ip; (bzt.*iq)(); //-------------------------------------------- //result : 白展堂 say "Hello" //-------------------------------------------- system("pause"); return 0; }
三.還有這種操作的函數(shù)指針(原創(chuàng))
實(shí)際上,對(duì)于計(jì)算機(jī)(特指馮諾依曼架構(gòu))而言,不管是int、char,還是什么指針、指令,其實(shí)都是一樣的一個(gè)數(shù),它們對(duì)CPU都沒(méi)什么區(qū)別,讓他們有區(qū)別的是我們對(duì)它的處理方式。
函數(shù)指針也是一樣的,理論上,我們可以對(duì)函數(shù)指針賦任何值。但是編譯器會(huì)阻止我們,不允許我們胡亂賦值??墒?,別忘了,C/C++語(yǔ)言之所以能夠稱(chēng)為接近底層的高級(jí)語(yǔ)言,其中一個(gè)原因就是它有著一個(gè)極其強(qiáng)大、危險(xiǎn),同時(shí)也是魅力十足的特性,就是“強(qiáng)制類(lèi)型轉(zhuǎn)換”。
四.進(jìn)階部分:
可能你會(huì)問(wèn),平常寫(xiě)程序都是直接寫(xiě)成如func()這樣的形式啊,并沒(méi)有感覺(jué)在操作一個(gè)指針。實(shí)際上,這是因?yàn)榫幾g器已經(jīng)幫我們實(shí)現(xiàn)了這一功能,并不需要我們操心。在VS的DEBUG模式,_cdecl調(diào)用約定下,一個(gè)簡(jiǎn)單的函數(shù)調(diào)用如下:
步驟1,使用CALL指令壓棧跳轉(zhuǎn):
func();
00974F8D E8 92C4 FF FF call func (0971424h)
步驟二,對(duì)應(yīng)地址實(shí)際上是存儲(chǔ)了一個(gè)跳轉(zhuǎn)指令,執(zhí)行跳轉(zhuǎn):
func:
00971424 E9 E737 00 00 jmp func (0974C10h)
步驟三,隨后跳轉(zhuǎn)到的才是實(shí)際的函數(shù)部分,函數(shù)首先將當(dāng)前寄存器狀態(tài)壓棧保護(hù)起來(lái),執(zhí)行程序,最后恢復(fù)現(xiàn)場(chǎng)并RET返回。
void func( void)
{
00974C10 55 push ebp
00974C11 8BEC mov ebp,esp
00974C13 81 ECC0 00 00 00 sub esp,0C0h
00974C19 53 push ebx
00974C1A 56 push esi
00974C1B 57 push edi
00974C1C 8D BD40 FF FF FF lea edi,[ebp-0C0h]
00974C22 B9 3000 00 00 mov ecx,30h
00974C27 B8 CCCC CC CC mov eax,0CCCCCCCCh
00974C2C F3AB rep stos dword ptr es:[edi]
i++;
00974C2E A0 80F6 97 00 mov al,byte ptr ds:[0097F680h]
00974C33 0401 add al,1
00974C35 A2 80F6 97 00 mov byte ptr ds:[0097F680h],al
}
00974C3A 5F pop edi
00974C3B 5E pop esi
00974C3C 5B pop ebx
00974C3D 8BE5 mov esp,ebp
00974C3F 5D pop ebp
00974C40 C3 ret
可以看出來(lái),實(shí)際上相當(dāng)繁瑣。這主要是因?yàn)镈EBUG模式下編譯器沒(méi)有進(jìn)行優(yōu)化。在RELEASE模式下:沒(méi)有步驟二,直接跳轉(zhuǎn)到函數(shù)體(我猜測(cè)DEBUG模式下多加入的步驟2是為了使用跳轉(zhuǎn)表存儲(chǔ)所有的函數(shù),方便調(diào)試);不會(huì)將所有寄存器壓棧,只會(huì)將函數(shù)中用到的壓棧;對(duì)于短函數(shù),會(huì)直接內(nèi)聯(lián)嵌入,不會(huì)進(jìn)行函數(shù)調(diào)用。
如果是函數(shù)指針,則步驟1的跳轉(zhuǎn)將變?yōu)椋?/p>
pfunc();
01195EFF 8BF4 mov esi,esp
01195F01 FF 1520 03 1A 01 call dword ptr ds:[11A0320h]
可以看出,實(shí)際上就是根據(jù)內(nèi)存中地址進(jìn)行了間接地址調(diào)用。DEBUG模式下,還會(huì)有以下兩句:
01195F07 3BF4 cmp esi,esp
01195F09 E8 2BB4 FF FF call __RTC_CheckEsp (01191339h)
這是檢查堆棧是否發(fā)生溢出用的,如果函數(shù)調(diào)用的壓棧、出棧中發(fā)生錯(cuò)誤使得前后堆棧位置不一致,則會(huì)調(diào)用__RTC_CheckEsp函數(shù)進(jìn)行錯(cuò)誤處理。
而這一過(guò)程中有個(gè)問(wèn)題,那就是我放入這個(gè)函數(shù)指針變量的數(shù)字,怎么保證它是一個(gè)函數(shù)呢?比如,要是我讓pfunc=0x12345678,那會(huì)出現(xiàn)什么結(jié)果呢?
很遺憾,事實(shí)上是,CPU根本無(wú)法保證它是一個(gè)函數(shù)。對(duì)于CPU來(lái)說(shuō),數(shù)據(jù)、指令都是一樣的。對(duì)一個(gè)數(shù)字0x12345678來(lái)說(shuō),究竟是把它看作一個(gè)整數(shù),還是4個(gè)char,還是一個(gè)float的一半,還是一個(gè)函數(shù)的地址,完全取決于編譯器和你。換句話(huà)說(shuō),一個(gè)數(shù)字究竟發(fā)揮什么作用,不是取決于其本身,而是取決于它的身份。這個(gè)身份有個(gè)名字——數(shù)據(jù)類(lèi)型。
嗯,這句話(huà)如果推廣的話(huà),能說(shuō)出很多看起來(lái)有哲理的話(huà)來(lái)呢。不過(guò)打住吧,我不想再打更多無(wú)關(guān)的字了(沒(méi)看我連函數(shù)指針的用法都懶得寫(xiě)了嗎?快進(jìn)入讓人激動(dòng)的部分吧)
對(duì)于C++這樣的靜態(tài)語(yǔ)言來(lái)說(shuō),當(dāng)程序編譯完成,編譯器就無(wú)法對(duì)程序做出任何改變了。所以,為了阻止你搞破壞,它就只能在編譯時(shí)阻止你。換句話(huà)說(shuō),就是必須做到“類(lèi)型匹配”。所以,C++要求所有的變量都需要聲明才能使用,這個(gè)過(guò)程實(shí)際上就是一個(gè)“給定身份的過(guò)程”。對(duì)于特定的指針,也就是特定的“身份”,編譯器會(huì)有不同的一套處理方式。比如,對(duì)于int型指針來(lái)說(shuō),pi++實(shí)際上是pi+=4。而對(duì)于char型指針來(lái)說(shuō),pi++則是pi+=1。
如:
charstr[4]=”abc”;
int *pi=str;
這是不行的,編譯器會(huì)識(shí)別出類(lèi)型匹配錯(cuò)誤,不允許通過(guò)編譯。
對(duì)于函數(shù)指針,如:
void func(int)
{
//…
}
void(*pfunc)(int,int);
pfunc=func;
pfunc();
也是不行的。對(duì)于func來(lái)說(shuō),在函數(shù)調(diào)用時(shí)會(huì)壓棧1次傳遞1個(gè)參數(shù),而pfunc,則會(huì)壓棧2次。如果編譯器讓以上程序通過(guò)編譯,則函數(shù)會(huì)壓棧2次傳遞參數(shù),返回時(shí)卻只會(huì)彈出1次。而在棧參數(shù)下的值,是程序應(yīng)該返回的位置,即調(diào)用時(shí)存儲(chǔ)的CALL語(yǔ)句的下一條指令。如果少?gòu)棾鲆淮?,則會(huì)將第一個(gè)參數(shù)作為返回位置,換句話(huà)說(shuō),程序就“跑飛”了。這是非常嚴(yán)重的錯(cuò)誤。因此,編譯器不會(huì)允許這樣的賦值。
編譯器花了這么大的力氣,看起來(lái)好像能夠堵住所有漏洞了。可是,C/C++語(yǔ)言之所以能夠稱(chēng)為接近底層的高級(jí)語(yǔ)言,其中一個(gè)原因就是它有著一個(gè)極其強(qiáng)大、危險(xiǎn),同時(shí)也是魅力十足的特性,就是“強(qiáng)制類(lèi)型轉(zhuǎn)換”。
類(lèi)型轉(zhuǎn)換非常常見(jiàn)。最常見(jiàn)的是每一本教材都會(huì)講的“隱式類(lèi)型轉(zhuǎn)換”。如:
int a=
【/d1reportAllClassLayout】
【Union聯(lián)合體】
【地址保護(hù)(80286加入)】
【NULL和nullptr】
【空類(lèi)大小為1】
// ConsoleApplication34.cpp : 定義控制臺(tái)應(yīng)用程序的入口點(diǎn)。 // #include "stdafx.h" #include <iostream> using namespace std; class Parent { public: Parent() { //cout << "我是爹" << endl; } virtual void print() { cout << "我是爹" << endl; } private: int a; }; class Child :public Parent { public: Child() { } void print() { cout << "我是兒子" << endl; } private: int b; }; int main() { //返回值類(lèi)型 (域作用符::*函數(shù)指針名稱(chēng))(方法參數(shù)簽名) 下面這個(gè)是無(wú)參的 //void (Child::*fptr)(); 無(wú)參的 //void (Child::*fptr)(int,int);有參的,自行領(lǐng)悟吧 //解釋?zhuān)簾o(wú)返回值類(lèi)型 作用域是屬于Child類(lèi)的函數(shù)指針 即:是指向Child類(lèi)的任何無(wú)參成員函數(shù)的函數(shù)指針 void (Child::*fptr)(); //最終綁定 指向 Child類(lèi)的print函數(shù)的函數(shù)指針 fptr= &Child::print;; Child arr[] = { Child(),Child(),Child() }; //Parent *p; //Child* c; //p = arr; //c = arr; //p->print(); //c->print(); //p++; //c++; //p->print(); //c->print(); //p++; //c++; //p->print(); //c->print(); (*arr.*fptr)(); //相當(dāng)于 arr[0].print(); (*(arr+1).*fptr)(); (*(arr + 2).*fptr)(); system("pause"); return 0; }
如對(duì)本文有疑問(wèn),請(qǐng)?zhí)峤坏浇涣髡搲?,廣大熱心網(wǎng)友會(huì)為你解答??! 點(diǎn)擊進(jìn)入論壇