萬字長文系統(tǒng)梳理C++函數(shù)指針
?本篇的內(nèi)容相對比較簡單 主要從語法的層面講解函數(shù)指針的使用以及應(yīng)用場景。都是些面向入門者的基礎(chǔ),大佬輕噴。
?
首先:什么是函數(shù)指針。
這個問題老生常談了,不用理解的多么復(fù)雜,它其實(shí)就是一個特殊的指針,它用于指向函數(shù)被加載到的內(nèi)存首地址,可用于實(shí)現(xiàn)函數(shù)調(diào)用。
聽上有點(diǎn)像函數(shù)名,函數(shù)名也是記錄了函數(shù)在內(nèi)存中的首地址,加()就可以調(diào)用。
不錯,不過函數(shù)指針和函數(shù)名還是有點(diǎn)區(qū)別的,他們雖然都指向了函數(shù)在內(nèi)存的入口地址,但函數(shù)指針本身是個指針變量,對他做&取地址的話會拿到這個變量本身的地址去。
而對函數(shù)名做&取址,得到的還是函數(shù)的入口地址。如果是類成員函數(shù)指針,差別更加明顯。
?關(guān)于函數(shù)名和函數(shù)指針的差異,找到一篇帖子介紹的比較深入,如果看完這篇文章你還沒暈的話,可以回過頭來去看看這位大佬的講解https://www.cnblogs.com/hellscream-yi/p/7943848.html
?
函數(shù)指針有啥用?
和通過函數(shù)名調(diào)用一樣,函數(shù)指針給我們提供了另一種調(diào)用函數(shù)的可能
而他又具備變量的特性,可以作為參數(shù)傳遞,可以函數(shù)返回
因此在一些直接通過函數(shù)名無法調(diào)用的場景下,函數(shù)指針就有了用武之地。
我們接下來還是先說說函數(shù)指針怎么寫,完后再提供一些具體應(yīng)用場景來說明它有什么用。
函數(shù)指針的寫法
大多數(shù)初學(xué)者包括我在內(nèi),潛意識里對于函數(shù)指針都有點(diǎn)抵觸,能不用的時候都盡量不用。
因?yàn)槲覀冇∠罄镆娺^的函數(shù)指針很可能是這樣的:
double?*?(*p1)(const?double?*?,?int?m);
int?(*funcArry[10])(int,?int);
typedef?char?*?(MyObject::*FUNC_PTR?)(const?chat?*?str);
void?*?(*?(?*?fp1)(int))[10];
#define?double?(*(*(*fp3)())[10])();
int?(*(*fp4())[10])();
甚至還有:
int?*(*(*fp)(int(*)(int,?int),?int(*)(int)))(int,?int,?int(*)(int,?double?*?(p1)(const?double?*?,?int?m)));
好在一般這種反人類的寫法,只會經(jīng)常出現(xiàn)在大學(xué)的期末試卷里,生產(chǎn)實(shí)踐中誰也不會把函數(shù)寫成這個鬼樣子。
不過這也奠定了我們內(nèi)心深處對于函數(shù)指針深深的抵觸和恐懼。
普通函數(shù)指針
言歸正傳,我們來說說函數(shù)指針的語法該怎么理解。
聲明
函數(shù)指針就是一種特殊的指針。
如果你要聲明一個變量:
int?a?;
而一個指針呢:
int?*a;
那一個函數(shù)指針,就是在一個變量指針的寫法基礎(chǔ)上加一個括號,告訴他這是一個指向函數(shù)的指針就可以:
int?(*a)();
這樣,a就是一個函數(shù)指針了。
這個括號(*a)一定要加,否則就成了int *a();編譯器會認(rèn)為這是一個 返回int *的函數(shù)a;
這時候呢,int (*a)();就聲明了一個函數(shù)指針變量a,它可以指向一個返回int,參數(shù)列表為空的函數(shù)。
前面的int,就是這個函數(shù)指針的返回值,a是變量名,最后一個()是參數(shù)列表。
賦值
直接將一個已經(jīng)定義的函數(shù)名,賦值給函數(shù)指針就可以:a = function;
當(dāng)然,直接把聲明定義和初始化寫在一起也可以,只是平常不多見這么寫:int (*a)() = function;
和上面先聲明再賦值是等價的。
調(diào)用
函數(shù)指針的變量,可以當(dāng)做函數(shù)名一樣被調(diào)用,所以直接:a();就相當(dāng)于調(diào)用了函數(shù)。
注意這是聲明的一個函數(shù)指針的變量,和函數(shù)的聲明有所區(qū)別。
因此你不能像定義一個函數(shù)一樣定義一個函數(shù)指針,你只能聲明出這個指針,然后給他賦值一個函數(shù)簽名匹配的已經(jīng)定義好的函數(shù)名:
int?function()??//?正確的函數(shù)聲明
{
????return?0;
}
?
int?(*a)()??????//?錯誤:這是一個變量,不能當(dāng)函數(shù)一樣定義
{
????return?0;
}
//你只能這樣:
int?(*a)();?????//聲明一個函數(shù)指針變量a,
int?main()
{
????a?=?function;???//給函數(shù)指針賦值。
????a();????????????//通過函數(shù)指針調(diào)用
????
????//?也可以直接把聲明和賦值寫在一起:這就像是 int i;和int * p = i;的區(qū)別
????int?(*b)()?=?function;
????b();
????return?0;
}?
稍微復(fù)雜一些的函數(shù)指針
給函數(shù)指針賦值的時候,對應(yīng)的函數(shù)簽名(返回值和參數(shù)列表)必須是和他的相匹配的。
如果對應(yīng)的函數(shù)原型比較復(fù)雜,相對應(yīng)的函數(shù)指針的寫法也會復(fù)雜一些。
這里循序漸進(jìn)地舉幾個相對復(fù)雜一些的例子:
//?最簡單的函數(shù)及其對應(yīng)的函數(shù)指針:
void?f();
void?(*f_ptr)();
//?復(fù)雜點(diǎn)的,帶返回值和參數(shù)列表,但都是基本類型
int?f(double?b,?int?i);
int?(*f_ptr)(double?b,?int?i);
//?返回值和參數(shù)帶上指針,再加上幾個const混淆一下
const?double?*?f(const?double?*?b2,?int?m);
const?double?*?(*f_ptr)(const?double?*?b2,?int?m);
//?再復(fù)雜一點(diǎn)點(diǎn),參數(shù)里加個函數(shù)指針?也不是很復(fù)雜,基本只要把函數(shù)名換成(*函數(shù)名)?就可以了
int?f(int?(*fp)(),int?a?);
int?(*f_ptr)(int?(*fp)(),int?a?);?
//?稍微再復(fù)雜一點(diǎn)點(diǎn),返回值是一個函數(shù)指針:(光是普通函數(shù)返回函數(shù)指針,語法就有點(diǎn)費(fèi)勁。我們一步一步來:)
////?首先搞一個返回void的普通函數(shù):
void?f();
////?假設(shè)返回一個函數(shù)指針,這個函數(shù)指針返回值和參數(shù)都為空。我們用一個函數(shù)指針替換掉返回值void就可以了
////?感覺應(yīng)該寫成這樣:void (*fp)() f();
////?但是這個樣子顯然過不了編譯的,得要變一下:
void?(*?f())();?????????//這就是一個參數(shù)為空,返回函數(shù)指針的函數(shù)。
void?(*(*f_ptr)())();????//把f替換成(*f_ptr),這就成了返回函數(shù)指針的函數(shù)指針。
//?其實(shí)寫成上面這個樣子,大多數(shù)人已經(jīng)懵逼了。
//?再往復(fù)雜的搞,真就徹底花了,比如返回值和參數(shù)里整上函數(shù)指針數(shù)組,函數(shù)指針參數(shù)里套函數(shù)指針,返回的函數(shù)指針返回值是個函數(shù)指針等等
//?這種的我們就不研究了。一方面項(xiàng)目中這么寫會挨罵,另一方面太復(fù)雜的我也不會。
從一開始的void f();,到最后成了這個void (*(*f_ptr)())();鬼樣子
說真的最后這種寫法我是正向推導(dǎo)過來的,如果是你維護(hù)別人的代碼,上來看到一個這void (*(*f_ptr)())();,恐怕得先罵一會兒娘才能正式開始工作
然而這卻只是返回函數(shù)指針的函數(shù)指針的最簡單的寫法,參數(shù)全為空,返回全為void,也不涉及指針數(shù)組,還完全沒有進(jìn)行太多反人類的語法變種。
好在,我們還是有辦法給他整的簡化一點(diǎn)的
把函數(shù)指針弄成一個自定義類型
我們把關(guān)注點(diǎn)聚焦到上面最后一個函數(shù)指針上,定義一個返回值是函數(shù)指針的函數(shù),完整的聲明加調(diào)用應(yīng)該是這樣的:
#include?
using?namespace?std;
void?aaa()
{
?cout?<"aaa"?<endl?;
}
void?(*?f())()??//?返回函數(shù)指針的函數(shù)f
{
?return?aaa;
}
int?main()
{
?void?(*(*f_ptr)())()?=?f;???//?返回函數(shù)指針的函數(shù)指針f_ptr
?//f_ptr()?返回一個函數(shù)指針,所以可以再跟一個()調(diào)用這個被返回出來的函數(shù)
?f_ptr()();?
????return?0;
}?
和我們平時返回int double不同,返回函數(shù)指針的這種語法實(shí)在太過抽象。
所以,我們能不能想辦法,把函數(shù)指針給搞成一種類型,然后就像int double一樣去使用?
當(dāng)然是可以的,這也是我們最常見的函數(shù)指針的玩法。我們可以使用typedef,直接將此函數(shù)指針處理成一個類型:
void (*f_ptr)();:這是定義了一個名為f_ptr的函數(shù)指針「變量」typedef void (*f_ptr)();:這是定義了一個名為f_ptr的函數(shù)指針「類型」,這個類型代表返回值為空,參數(shù)為空的函數(shù)指針類型。有些地方覺得f_ptr的名字起得不好,還會再用 #define FUNC_PTR f_ptr這樣搞一下,后面代碼中統(tǒng)一使用FUNC_PTR代表這個函數(shù)指針類型。
區(qū)別是什么呢?如果類比我們熟悉的普通變量類型int:
那上面的第一行,就相當(dāng)于 int a;,a是一個整型變量;第二行呢,就相當(dāng)于 typedef int a,這樣一來a,就相當(dāng)于是int,可以用a i; a j;'的方式聲明整型變量i,j;
有了這個f_ptr類型,上面很多復(fù)雜的定義寫法就可以簡化,而且語義一下子就清楚很多了:
聲明一個函數(shù)指針并賦值:
//?void?(*fp)()?=?func;
f_ptr?fp?=?func?;?
函數(shù)參數(shù)里包含函數(shù)指針:
//int?f(int?(*fp)(),int?a?);
int?f(f_ptr?fp,?int?a);
返回值是函數(shù)指針,我們直接把上面那段完整的代碼通過 typedef重寫一下:
//函數(shù)定義:
#include?
using?namespace?std;
typedef?void?(*f_ptr)();
void?aaa()
{
????cout?<"aaa"?<endl?;
}
//?void?(*?f())()
f_ptr?f()???//返回值是函數(shù)指針的函數(shù)定義,?語義一目了然
{
????return?aaa;
}
int?main()
{
????//?void?(*(*f_ptr)())()?=?f;
????//?f_ptr()();?
????f_ptr?(*ff)()?=?f;?//返回函數(shù)指針的函數(shù)指針?
????ff()();
????return?0;
}?
當(dāng)然還可以寫的更抽象一些,把返回函數(shù)指針的函數(shù)指針也typedef一下:typedef void (*(*F_PTR)())();
這下定義的時候直接把上面的f_ptr (*ff)() = f;換成:F_PTR ff = f ;,更是簡潔明快。
到這里呢,我們就基本掌握了函數(shù)指針的寫法和用法,其實(shí)很簡單。
稍微總結(jié)一下上面的內(nèi)容:
如何聲明一個簡單的函數(shù)指針: void (*f_ptr)()給函數(shù)指針賦值: fp = function;function是一個已經(jīng)定義的函數(shù)名通過函數(shù)指針調(diào)用函數(shù): fp();復(fù)雜一些的函數(shù)指針: 復(fù)雜的返回值 多個參數(shù) 參數(shù)里帶函數(shù)指針 返回值是函數(shù)指針的情況。 這種寫法太麻煩了怎么辦?把函數(shù)指針搞成一個類型: typedef void (*f_ptr)();用這個類型聲明一個函數(shù)指針: f_ptr fp;返回這個類型函數(shù)指針的函數(shù) f_ptr f();參數(shù)包含這個類型函數(shù)指針的函數(shù): int f(f_ptr fp, int a);套娃函數(shù)指針————返回函數(shù)指針的函數(shù)的函數(shù)指針: f_ptr (*ff)();
再把數(shù)組扯進(jìn)來
之所以一直不扯,是因?yàn)楹瘮?shù)指針和數(shù)組結(jié)合在一起的話,可讀性一下下降了好幾個數(shù)量級
掌握了上面的寫法,我們再把復(fù)雜度提升億點(diǎn)點(diǎn):定義一個長度為10數(shù)組,數(shù)組中的元素是函數(shù)指針:
長度為10的數(shù)組: int a[10];那么長度為10的函數(shù)指針數(shù)組,就先把 int換成函數(shù)指針:void (*f_ptr)() a[10];當(dāng)然函數(shù)指針的聲明時,函數(shù)指針名就是變量名,所以這個 a就沒用了,應(yīng)該寫成這樣:void (*f_ptr)()[10]
遺憾的是這種想當(dāng)然的寫法當(dāng)然過不了編譯,一個數(shù)組聲明的時候,[]要緊跟在變量名之后
所以正確的聲明、賦值與調(diào)用寫法是:
void?(*f_ptr[10])();????//?定義一個長度為10的數(shù)組,數(shù)組中的元素類型是函數(shù)指針
f_ptr[3]?=?function;????//?每一個元素都可以指向一個函數(shù),我們賦值給第數(shù)組中的第四個元素函數(shù)function的地址
f_ptr[3]();?????????????//?通過數(shù)組下標(biāo)拿到函數(shù)指針,通過函數(shù)指針調(diào)用函數(shù)。?這里相當(dāng)于調(diào)用了function();
當(dāng)然,上面提到了typedef大法,可以幫助我們簡化上面這種寫法:(說是簡化,其實(shí)寫的更多,但是可讀性更好)
typedef?void?(*f_ptr)();
f_ptr?f_tpr_arrya[10];??????//把f_ptr當(dāng)做一種類型后,聲明函數(shù)指針數(shù)組,就可聲明普通的int數(shù)組看上去沒啥區(qū)別了。
f_tpr_arrya[3]?=?function;
f_tpr_arrya[3]();?????????????
這是最基本的函數(shù)指針數(shù)組,他里面存放的元素是簽名最為簡單的函數(shù)指針。
如果這個數(shù)組里記錄的函數(shù)指針簽名復(fù)雜一些,一旦套起娃來那畫風(fēng)將可以用恐怖來形容。
這里不深入探討了,舉幾個例子:(主要摘錄自:https://www.xuebuyuan.com/1238896.html)
const char *(*f_ptr[10])(int a[], double * b)長度為10的數(shù)組,數(shù)組元素為返回const char *,參數(shù)(int [],double *)的函數(shù)指針。const char *(*f_ptr[10])(double * (*b[10])(int ,int )):長度為10的數(shù)組,數(shù)組元素為返回const char *,參數(shù)為“返回double*參數(shù)為int,int的函數(shù)指針數(shù)組”的函數(shù)指針。Void * (* ( * fp)(int))[10]:fp是一個函數(shù)指針,它指向的函數(shù)帶有一個int型的參數(shù),返回值為一個指向含有10個void指針數(shù)組的指針。void * (* ( * fp[10])(int))[10]:fp是一個長度為10的函數(shù)指針數(shù)組,元素里的函數(shù)指針指向的函數(shù)帶有一個int型的參數(shù),返回值為一個指向含有10個void指針數(shù)組的指針。Void * ( * fp)(int)[10]:fp是一個函數(shù)指針,它指向的函數(shù)帶有一個int型的參數(shù),返回值為一個指向含有10個void類型的數(shù)組的指針。Void ( * fp)(int)[10]:fp是一個函數(shù)指針,它指向的函數(shù)帶有一個int型的參數(shù),返回值為一個有10個void類型的數(shù)組。double (*(*(*fp)())[10])():fp是一個函數(shù)指針,它指向的函數(shù)不帶參數(shù),返回值是一個指針,該指針指向一個指針數(shù)組,該指針數(shù)組容量為10。指針數(shù)組中的指針又是函數(shù)指針,該指針指向的函數(shù)不帶參數(shù),返回值為double。int (*(*fp())[10])();:fp的返回值是一個指針,該指針指向含有10個函數(shù)指針的數(shù)組。數(shù)組中的指針指向的函數(shù)不帶參數(shù),返回值為int。
可以看到函數(shù)指針一和數(shù)組扯到一起,寫法抽象程度一下子就上了一個量級。
平時寫代碼的時候,最好還是用typedef把函數(shù)指針的類型定義一下,不要寫的太花。
雖然我從來喜歡大道至簡,但是函數(shù)指針數(shù)組這種搞法確實(shí)還是有一定的應(yīng)用場景的。
比如我們后面將要提到的轉(zhuǎn)移表
類的函數(shù)指針
函數(shù)指針是指向函數(shù)的指針,而我們上面提到的函數(shù),一直都是面向過程的函數(shù),對于面向?qū)ο蟮暮瘮?shù)還只字未提。
我們下面僅僅討論一下c++中類的函數(shù)指針的最簡單的語法規(guī)范,上面那些高深莫測的套娃函數(shù)指針,就不和類函數(shù)指針扯到一起了。
面向?qū)ο蟮木幊讨校瘮?shù)被新搞出了兩種花樣:「靜態(tài)函數(shù)和成員函數(shù)」
關(guān)于靜態(tài)函數(shù)和成員函數(shù)這兩種函數(shù)的區(qū)別也是老生常談的問題,我們關(guān)于函數(shù)指針的討論,在這里只需要記住一句最核心的一句話:「靜態(tài)函數(shù)沒有this指針。」
類靜態(tài)成員函數(shù)指針
類的靜態(tài)成員函數(shù)沒有this指針,它的存儲方式和普通的函數(shù)是一樣的,可以取得的是該函數(shù)在內(nèi)存中的實(shí)際地址
所以靜態(tài)的成員函數(shù)指針的聲明和調(diào)用,和普通函數(shù)指針沒有任何區(qū)別:
聲明: void (*static_fptr)();調(diào)用: static_fptr();
唯一有區(qū)別的,就是賦值。因?yàn)橐獋鞯氖且粋€類的靜態(tài)成員函數(shù)的地址,所以賦值的時候,要加上類名限定:
void (*static_fptr)() = &Test::staticFunc;
同樣,通過typedef把它搞成類型用法和之前也一樣,可以使代碼更清晰。
類成員函數(shù)指針
與靜態(tài)函數(shù)不同,成員函數(shù)在被調(diào)用時,必須要提供this指針。
因?yàn)樵谒徽{(diào)用之前,自己也不知道哪個對象的此函數(shù)被調(diào)用。所以通過&拿到的不是實(shí)際的內(nèi)存地址。
只有調(diào)用的時候,C++才會結(jié)合this指針通過固定的偏移量找到函數(shù)的真實(shí)地址調(diào)用。
為了支持這種調(diào)用方式,這里C++給專門提供了特殊的幾個操作符:::* .* ->*
聲明: void (Test::*fptr)();,類成員函數(shù)指針的聲明,就必須加上類名限定,這就聲明了一個函數(shù)指針變量fptr,他只能指向Test類的成員函數(shù)。賦值: fptr = &Test::function調(diào)用:類的成員函數(shù)是無法直接調(diào)用的,必須要使用對象或者對象指針調(diào)用(這樣函數(shù)才能通過對象獲取到this指針)。 (t.*fptr)();,t是Test類的一個實(shí)例,通過對象調(diào)用。(pt->*fptr)();,pt是一個指向Test類對象的指針,通過指針調(diào)用。
C++成員函數(shù)的調(diào)用需要至少3個要素:
this指針; 函數(shù)參數(shù)(也許為空); 函數(shù)地址。
上面的調(diào)用中,->*和.*運(yùn)算符之前的對象指針提供了this(和真正使用this并不完全一致)
參數(shù)在括號內(nèi)提供,fptr則提供了函數(shù)地址。
指向虛函數(shù)的函數(shù)指針
虛函數(shù)其實(shí)就是一種特殊的成員函數(shù),所以指向虛函數(shù)的函數(shù)指針寫法,同上。
不一樣的是:「虛函數(shù)函數(shù)指針同樣具有虛函數(shù)的特性——多態(tài):基類的成員函數(shù)指針可以賦值給繼承類的成員函數(shù)指針。」
另外,指向虛函數(shù)的函數(shù)指針在涉及到多繼承和指針強(qiáng)轉(zhuǎn)的問題時,使用不當(dāng)會踩到大坑:
不要使用 static_cast將繼承類的成員函數(shù)指針賦值給基類成員函數(shù)指針,如果一定要使用,首先確定沒有問題。(這條可能會限制代碼的可擴(kuò)展性。)如果一定要使用 static_cast, 注意不要使用多繼承。如果一定要使用多繼承的話,不要把一個基類的成員函數(shù)指針賦值給另一個基類的函數(shù)指針。 單繼承要么全部不使用虛函數(shù),要么全部使用虛函數(shù)。不要使用非虛基類,卻讓子類包含虛函數(shù)。
這里我們只提一下結(jié)論,具體這些坑出現(xiàn)的原因,感興趣的可以看看這篇比較深入的文章:https://blog.csdn.net/ym19860303/article/details/8586971
能否搞出指向構(gòu)造函數(shù)和析構(gòu)函數(shù)的函數(shù)指針?
我反正是沒聽說過有這么用的
我知道你想都沒這么想過
但是總有SB面試會這么問你......
答案是不行,C++標(biāo)準(zhǔn)明確規(guī)定:The address of a constructor or destructor shall not be taken.
也可以隨便寫一個驗(yàn)證一下,編譯報錯也很明確:
語法總結(jié)
類函數(shù)指針的語法相當(dāng)嚴(yán)格:
對于類內(nèi)成員的函數(shù)指針的使用和獲取,要注意的是:
不能使用括號:例如 &(ClassName::foo)不對。必須有限定符:例如 &foo不對。即使在類ClassName的作用域內(nèi)也不行。必須使用取地址符號:例如直接寫 ClassName::foo不行。(雖然普通函數(shù)指針可以這樣)
所以,必須要這樣寫:&ClassName::foo。
對于類內(nèi)成員函數(shù)指針的調(diào)用,還要注意:(t.*fptr)();和(pt->*fptr)();必須要加括號
因?yàn)檎{(diào)用的優(yōu)先級比.*,->*高,不加括號就成了:t.*fptr();,這其實(shí)相當(dāng)于:t.*(fptr());。
把后面當(dāng)成一個整體,然而fptr并不是一個函數(shù),編譯會直接失敗。
::* .* ->*并不只是針對函數(shù)指針,如果在類外部聲明指向類內(nèi)成員「變量」的指針的話,也要用這幾個操作符才行。
一個非常簡單的實(shí)例
class?Test
{
public?:
????void?function?(){cout?<"member?function?"?<endl;}???????????//?類成員函數(shù)
????static?void?s_function(){cout?<"static?function?"?<endl;}???//?類靜態(tài)成員函數(shù)
};
int?main()
{
????Test?t;?????????????//?類對象
????Test?*pt?=?&t;??????//?對象指針
????t.function();???????//?通過對象調(diào)用成員函數(shù)
????Test::s_function();?//?調(diào)用靜態(tài)成員函數(shù)
????void?(*s_fptr)()?=?&Test::s_function;???????????//?靜態(tài)成員函數(shù)指針
????s_fptr();???????????????????????????????????????//?通過?靜態(tài)成員函數(shù)指針調(diào)用靜態(tài)成員函數(shù)
????void?(Test::*fptr)()?=?&Test::function;?????????//?成員函數(shù)指針
????(t.*fptr)();????????????????????????????????????//?經(jīng)由對象的成員函數(shù)指針調(diào)用函數(shù)
????(pt->*fptr)();??????????????????????????????????//?經(jīng)由對象指針的成員函數(shù)指針調(diào)用函數(shù)????????????????????
????return?0;
}?
應(yīng)用場景
函數(shù)指針的應(yīng)用在生產(chǎn)實(shí)踐中其實(shí)是非常廣泛的。
網(wǎng)上很多關(guān)于函數(shù)指針的應(yīng)用場景的講解都會自己設(shè)計(jì)個場景講解一小段。
我這里就不班門弄斧了,給大家找?guī)讉€我工作中遇見過的開源項(xiàng)目,看看他們的函數(shù)指針是怎么用的:
應(yīng)用場景一、轉(zhuǎn)移表:
玩過linux的同學(xué)一定都用敲很多命令,有些命令行工具特別強(qiáng)大,比如像什么sed,awk等等。
這些工具無一例都可以對復(fù)雜的命令行參數(shù)進(jìn)行精準(zhǔn)解析。
如果你自己寫過命令行解析的程序就會發(fā)現(xiàn)這并不是一件容易的事情。
我在研究多線程打包的時候有看過dpkg的源碼。這里可以簡單講一下:(代碼來源:https://git.dpkg.org/git/dpkg/dpkg.git)
dpkg是Linux Debian系系統(tǒng)自帶的包管理工具,管理整個系統(tǒng)的安裝包安裝卸載,常見的用法有:
dpkg -i 包名或dpkg --install 包名安裝dpkg -l列出所有包詳細(xì)信息dpkg -l 包名列出指定包詳細(xì)信息dpkg --purge 軟件名或者dpkg -P 軟件名卸載軟件復(fù)雜一點(diǎn)的組合用法: dpkg -D2 --ignore-depends=libgtk --force -i 包名等等。
像這種命令工具的邏輯如果讓我寫,指定滿屏幕的if else把自己也繞暈。
但是在dpkg的源碼里,就用了一種比較高端的玩法
(其實(shí)大多數(shù)命令行工具在解析命令參數(shù)的時候都有用這種辦法,這里我為了好懂一點(diǎn)有所改動,源碼比這個還要晦澀很多,純C的項(xiàng)目屬實(shí)有點(diǎn)難啃):
struct?cmdinfo?{????????????????//?命令結(jié)構(gòu)體,每一種命令對應(yīng)一個實(shí)例,存放命令本身的字符串以及執(zhí)行的函數(shù)指針等
??const?char?*olong;
??char?oshort;
??/*
???*?0?=?Normal????(-o,?--option)
???*?1?=?Standard?value???(-o=value,?--option=value?or
???*??????-o?value,?--option?value)
???*?2?=?Option?string?continued?(--option-value)
???*/
??int?takesvalue;
??int?*iassignto;
??const?char?**sassignto;
??void?(*call)(const?struct?cmdinfo*,?const?char?*value);
??int?arg_int;
??void?*arg_ptr;
??action_func?*action;
};
//?........
//兩個宏,就是簡化一下寫法而已。
#define?ACTION(longopt,?shortopt,?code,?func)?\
?{?longopt,?shortopt,?0,?NULL,?NULL,?setaction,?code,?NULL,?func?}
#define?ACTIONBACKEND(longopt,?shortopt,?backend)?\
?{?longopt,?shortopt,?0,?NULL,?NULL,?setaction,?0,?(void?*)backend,?execbackend?}
//?指令的結(jié)構(gòu)體數(shù)組,dpkg所有支持的參數(shù)都收錄在這里。
static?const?struct?cmdinfo?cmdinfos[]=?{
#define?ACTIONBACKEND(longopt,?shortopt,?backend)?\
?{?longopt,?shortopt,?0,?NULL,?NULL,?setaction,?0,?(void?*)backend,?execbackend?}
??ACTION(?"install",????????????????????????'i',?act_install,??????????????archivefiles????),
??//?......
??ACTION(?"remove",?????????????????????????'r',?act_remove,???????????????packages????????),
??ACTION(?"purge",??????????????????????????'P',?act_purge,????????????????packages????????),
??ACTIONBACKEND(?"list",????????????????????'l',?"dpkg-query"),
??//?......
??{?"ignore-depends",????0,???1,?NULL,??????????NULL,??????set_ignore_depends,?0?},
??//?.......
??{?"debug",?????????????'D',?1,?NULL,??????????NULL,??????set_debug,?????0?},
??{?"help",??????????????'?',?0,?NULL,??????????NULL,??????usage,?????????0?},
??{?"version",???????????0,???0,?NULL,??????????NULL,??????printversion,??0?},
??//?.......
??{?NULL,????????????????0,???0,?NULL,??????????NULL,??????NULL,??????????0?}
};
乍一看有點(diǎn)眼暈,沒事,一步一步來:
ACTION和ACTIONBACKEND都是宏,最后他們都變成了一個cmdinfo結(jié)構(gòu)體的定義。所以可以看做和它下面的一樣。
這段程序?yàn)榱四軐?shí)現(xiàn)不同的參數(shù)對應(yīng)不同的處理,用了一個結(jié)構(gòu)體數(shù)組
每一個結(jié)構(gòu)體里面,存了固定的命令行參數(shù)和他對應(yīng)的處理函數(shù)的「函數(shù)指針」。比如說這行:
ACTION(?"install",????????????????????????'i',?act_install,??????????????archivefiles????),
這個ACTION是個宏定義,它替換后的樣子就是:
{?"install",?'i',?0,?NULL,?NULL,?setaction,?act_install,NULL,?archivefiles?},
其他不用管,你只需要知道程序會自動解析這個結(jié)構(gòu)體
第一個install代表如果匹配到--install的寫法,第二個i表示匹配到-i的寫法。所以命令里-i和--install是一樣的操作
最后一個參數(shù)archivefiles就是如果匹配到前面的參數(shù),要執(zhí)行的函數(shù)(這是個「函數(shù)指針」,所以可以直接傳遞函數(shù)名進(jìn)去)。
至于解析的具體的實(shí)現(xiàn),其實(shí)你都不用太關(guān)注細(xì)節(jié),你只需要知道這么寫能實(shí)現(xiàn)功能就可以。
dpkg在執(zhí)行的時候,main函數(shù)把接收到的所有參數(shù)都交給解析函數(shù)處理
解析函數(shù)就會拿出每一組參數(shù),并且遍歷這個結(jié)構(gòu)體數(shù)組去比對
如果匹配到了。直接調(diào)用對應(yīng)的函數(shù)指針。
最后的效果就是,當(dāng)程序檢測到你傳遞了-i或者--install參數(shù)時,就調(diào)用archivefiles執(zhí)行相應(yīng)的功能
那么現(xiàn)在如果讓你給dpkg命令行添加一個參數(shù)的支持,比如說打印一句hello world你怎么做?
你只需要寫一個名為hello的函數(shù),然后把參數(shù)和函數(shù)名添加在這個結(jié)構(gòu)體數(shù)組里就可以
解析是全自動而且可靈活擴(kuò)展的,你根本不需要知道太多細(xì)節(jié),也不需要做任何多余的改動:
int?hello_world(const?char?*?const?*argv)?//?函數(shù)簽名要和定義好的函數(shù)指針保持一致
{
??printf("hello?world!\n");
??exit(0);??????????//?因?yàn)橹淮蛴⌒畔ⅲ柚筪pkg的后續(xù)代碼執(zhí)行,這里直接退出
}
//?......?
static?const?struct?cmdinfo?cmdinfos[]=?{
??//?.......
??{?"hello",????????????'H',??0,?NULL,??????????NULL,??????hello_world,?0?},?//?新添加的一行,位置只要在結(jié)尾行上面就行
??//?.......
??{?NULL,????????????????0,???0,?NULL,??????????NULL,??????NULL,??????????0?}
};
運(yùn)行結(jié)果:
在這里函數(shù)指針就為這種靈活的調(diào)用方式提供了強(qiáng)有力的支持!
這個功能實(shí)現(xiàn)的核心,就是在結(jié)構(gòu)體里存放了一個函數(shù)指針變量。
在代碼執(zhí)行的時候,通過匹配到不同的參數(shù),就找不同的函數(shù)調(diào)用來執(zhí)行不同的功能。
相比于寫if else switch case,這種寫法不僅高端而且靈活高效,擴(kuò)展性又非常好,而且還很簡潔易讀(對于有一定基礎(chǔ)的同學(xué)而言)
?很多網(wǎng)上的資料對于轉(zhuǎn)移表的講解,都是一個單純的函數(shù)指針數(shù)組,這里是一個相對復(fù)雜點(diǎn)的“包含函數(shù)指針的結(jié)構(gòu)體數(shù)組”,我也把他歸為轉(zhuǎn)移表里面了。
?
我個人認(rèn)為這么歸類是合理的,但是因?yàn)闆]找到官方有“轉(zhuǎn)移表”的說法和明確定義,不知道這里這么歸類是否合適。關(guān)于這一點(diǎn)歡迎感興趣的小伙伴調(diào)研補(bǔ)充。
應(yīng)用場景二、回調(diào)函數(shù)
二.1 函數(shù)指針回調(diào)
linux系統(tǒng)編程中,可以使用signal函數(shù)讓程序具備處理內(nèi)置系統(tǒng)信號的能力。
比如像這樣一個程序(linux上玩,windows編不過哦):
#include?
#include?"signal.h"
using?namespace?std;
void?ctrl_c_is_pressed(int?signo)
{
?cout?<"小朋友,你是否有很多問號?"?<endl;
}
int?main()
{
?signal(SIGINT,ctrl_c_is_pressed);
?while(true);
????return?0;
}?
它執(zhí)行起來效果會非常詭異,你會發(fā)現(xiàn)萬能的Ctrl+C停不掉它:
這就是一個經(jīng)典的回調(diào)函數(shù)的應(yīng)用,我們通過signal函數(shù)給信號SIGINT(也就是Ctrl+C被按下時,系統(tǒng)實(shí)際發(fā)送的信號)注冊了一個處理函數(shù)ctrl_c_is_pressed
每當(dāng)程序收到SIGINT信號時,它就會執(zhí)行我們注冊的這個函數(shù)。(如果我們沒有注冊,他會執(zhí)行系統(tǒng)內(nèi)置的默認(rèn)行為,也就是中斷程序)
我這里說的回調(diào)函數(shù),就是通過函數(shù)指針來實(shí)現(xiàn)的,你可以看到我在注冊的時候直接傳了函數(shù)名稱進(jìn)去,并把它和SIGINT信號綁定到了一起。
然后每當(dāng)程序收到SIGINT信號的時候,他就會調(diào)用我們注冊好的函數(shù)。(回調(diào)回調(diào),就是這個意思)
其實(shí)在Linux系統(tǒng)源碼中,signal的函數(shù)原型是這樣的(Ubuntu 16.04,不同系統(tǒng)會有差異):
/*?Set?the?handler?for?the?signal?SIG?to?HANDLER,?returning?the?old
???handler,?or?SIG_ERR?on?error.
???By?default?`signal'?has?the?BSD?semantic.??*/
__BEGIN_NAMESPACE_STD
#ifdef?__USE_MISC
extern?__sighandler_t?signal?(int?__sig,?__sighandler_t?__handler)
?????__THROW;
#else
拋去你不認(rèn)識的部分,只看函數(shù)聲明:__sighandler_t signal (int __sig, __sighandler_t __handler);這個__sighandler_t你再往下挖就會驚喜的發(fā)現(xiàn):
/*?Type?of?a?signal?handler.??*/
typedef?void?(*__sighandler_t)?(int);
這下認(rèn)識了吧,signal就是一個返回函數(shù)指針的函數(shù),他還包含兩個參數(shù),一個是int,另一個是函數(shù)指針。
這個函數(shù)指針可以指向一個參數(shù)為int,返回為空的函數(shù),所以我們上面寫的ctrl_c_is_pressed可以直接傳進(jìn)去
在很多文章里或者有些舊版的代碼里寫的都是這樣的:
void?(*signal(int?signo,?void?(*func)(int)))(int);
其實(shí)就是上面,沒有typedef的版本。
二.2 類成員函數(shù)指針回調(diào)
上面這個是函數(shù)指針回調(diào),下面看一個類成員函數(shù)指針的回調(diào)。
相信不少小伙伴在大學(xué)的時候多多少少玩過cocos2d,unity3d之類的做過小游戲。
這里簡單拉出cocos2d-x的按鍵回調(diào)的代碼看看它是怎么應(yīng)用函數(shù)指針的:
使用cocos2d做游戲,如果你想在游戲屏幕上加一個按鈕,你需要這么寫:
CCMenuItemImage?*pCloseItem?=?CCMenuItemImage::create(
????????????????????????????????????"CloseNormal.png",??????????????????????????????//?正常狀態(tài)顯示的圖片
????????????????????????????????????"CloseSelected.png",????????????????????????????//?被按下時顯示的圖片
????????????????????????????????????this,???????????????????????????????????????????//?回調(diào)的執(zhí)行者
????????????????????????????????????menu_selector(HelloWorld::menuCloseCallback));??//?回調(diào)執(zhí)行的操作。
這里最重要的是后面兩個參數(shù),分別是回調(diào)的執(zhí)行者和執(zhí)行的函數(shù)名。
你可以從功能上來理解:我們點(diǎn)擊一個按鈕,就要觸發(fā)某個功能,比如開始游戲,關(guān)閉游戲等等。
這個功能的觸發(fā)需要兩個要素:「【誰】【做什么事情】」
所以這里每一個按鈕生成的時候,都需要指定兩個必要的參數(shù),一個是“誰”,另一個就是“做什么”。
只要你指定過這兩個參數(shù),代碼底層會自動處理,在按鈕被點(diǎn)擊的時候,就讓“誰”執(zhí)行“指定操作”。
比如我們上面的代碼,就是讓“當(dāng)前窗體”執(zhí)行“關(guān)閉操作”。
和上面的signal注冊回調(diào)本質(zhì)上是一樣的,不同的是,這里的回調(diào)是跨類回調(diào),你需要在CCMenuItemImage這個類里,調(diào)用其他類里面的某個函數(shù)
上面我們也講了,非靜態(tài)的成員函數(shù)在指針調(diào)用,必須要傳遞this指針。所以這種回調(diào)機(jī)制至少要傳兩個參數(shù),一個是函數(shù)地址,一個是this指針。
這種跨類回調(diào)也是函數(shù)指針的一個經(jīng)典應(yīng)用,而且在編程實(shí)踐中的應(yīng)用可以說非常廣泛。
?這里只簡單說明一下這種跨類回調(diào)的場景下,用到了函數(shù)指針。至于他底層的實(shí)現(xiàn)的機(jī)制,詳解的話足夠單拉一篇文章了,這里先留個坑,后期寫好補(bǔ)上。
?
上面看到的是cocos2d-x 2.X版本的寫法,這也是官網(wǎng)上可以下載到的第二代中最新的2.2.6的版本。官方早就已經(jīng)不再維護(hù),不過用作代碼的研讀和學(xué)習(xí)非常有用。
如果你能看懂我上面的講解就會明白,cocos2d-x 這個版本的代碼可讀性非常好,我感覺非常適合我這種稍微有點(diǎn)基礎(chǔ)的初學(xué)者學(xué)習(xí)。
到了3.x版本里(我下的3.17.2),這種跨類的回調(diào)機(jī)制玩法也早已換成了風(fēng)騷萬倍的C++11的玩法:
auto?closeItem?=?MenuItemImage::create(
????????????????????????"CloseNormal.png",
????????????????????????"CloseSelected.png",
????????????????????????CC_CALLBACK_1(HelloWorld::menuCloseCallback,this));
感覺寫法上差別好像不太大,其實(shí)底層的實(shí)現(xiàn)完全換了一種機(jī)制。上面2.X版本,使用的跨類函數(shù)指針進(jìn)行回調(diào)。下面這種CC_CALLBACK_1寫法,底層已經(jīng)是C++11的bind+std::function了
應(yīng)用場景三、反射
上面這段cocos2d創(chuàng)建按鈕的代碼,如果有同學(xué)用過cocos2d-java的話就會知道,在java里等價的寫法應(yīng)該是這樣的:
CCMenuItemImage?closeMenu?=?CCMenuItemImage.item(
????????????????????????????????????"CloseNormal.png",?
????????????????????????????????????"CloseSelected.png",?
????????????????????????????????????this,?
????????????????????????????????????"close");
注意這個地方最后一個參數(shù),在C++中它要傳一個函數(shù)指針,不過到j(luò)ava里,它傳一個函數(shù)名的字符串就可以了,這個close就是函數(shù)名。
這里就是用了java的反射機(jī)制,可以直接把字符串映射成真正的函數(shù)地址并實(shí)現(xiàn)調(diào)用。
在C++當(dāng)中,語言本身并不提供反射機(jī)制。但是仍然可以通過函數(shù)指針實(shí)現(xiàn),在很多C++實(shí)現(xiàn)的中間件中都有反射的實(shí)現(xiàn),我平時了解到的,使用C++實(shí)現(xiàn)的最完善的動態(tài)反射機(jī)制當(dāng)屬Q(mào)t的QMetaObject::invokeMethod();
反射最大的好處,就是讓你的代碼一般人輕易看不懂,IDE里Ctrl+鼠標(biāo)左鍵跳轉(zhuǎn)不過去。
維護(hù)難度一上來,你的價值就體現(xiàn)出來了,等待你的將是升職加薪,迎娶白富培走向人生....扯遠(yuǎn)了。
反射最大的好處,是讓你的代碼靈活度和可擴(kuò)展性大大提升。不過相對的,可維護(hù)性也有一定的損失。
有了反射之后,你完全可以通過QMetaObject::invokeMethod("function_name");來進(jìn)行函數(shù)調(diào)用。
之所以說這么做靈活,是因?yàn)樽址銐蜢`活。
比如你寫了十個函數(shù),名字分別是function_1、function_2、function_3、function_4.....
為了實(shí)現(xiàn)分別調(diào)用,沒有反射你就需要寫十次調(diào)用或者用轉(zhuǎn)移表
有了反射,你可以用字符串拼接的方式"function_"+i 拼出函數(shù)名,然后invokeMethod來調(diào)用。
和上面的cocos2d一樣,這里就先了解一下反射這個函數(shù)指針的應(yīng)用場景就好,就不深入講實(shí)現(xiàn)原理了。
(實(shí)在是因?yàn)镼t這個invokeMethod的實(shí)現(xiàn)機(jī)制啃了一次不得要領(lǐng),就不敢深入瞎講了。)
最后
以上就是本篇關(guān)于C++函數(shù)指針講解的全部內(nèi)容,一篇典型收藏吃灰系列的文章
就是簡單捋了一下函數(shù)指針的寫法、功能以及應(yīng)用
沒什么深度,所以應(yīng)該也沒什么嚴(yán)重的誤導(dǎo)和錯誤
上面提到了在cocos2d-x的新版本中用std::function代替了函數(shù)指針,這也是現(xiàn)在C++框架和應(yīng)用的主流寫法
C++11提供的std::function將從語法層面為函數(shù)指針的使用提供強(qiáng)大的支持,并且代碼的可讀性也明顯提升。
計(jì)劃將在近期再寫一篇文章對std::function進(jìn)行一個簡單的梳理,會和本篇一樣沒什么難度深度,歡迎關(guān)注。
最后額外補(bǔ)充一個彩蛋:如果你需要一個聲明函數(shù)指針指向某個函數(shù),但這個函數(shù)實(shí)在太過復(fù)雜以至于它的函數(shù)指針聲明你不會寫
那你可以直接:auto f = functionname(僅限C++11以上)
參考鏈接:
https://blog.csdn.net/qq_42128241/article/details/81610124 https://www.cnblogs.com/yangyuliufeng/p/10720417.html https://www.cnblogs.com/hellscream-yi/p/7943848.html https://blog.csdn.net/tangyangyu123/article/details/89978915 https://blog.csdn.net/zhuxiufenghust/article/details/6543652?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2 https://www.cnblogs.com/yangjiquan/p/11465376.html https://www.xuebuyuan.com/1238896.html https://blog.csdn.net/shenhuxi_yu/article/details/75948887 https://blog.csdn.net/qq_28773183/article/details/78262444 https://isocpp.org/wiki/faq/pointers-to-members https://stackoverflow.com/questions/2402579/function-pointer-to-member-function https://www.codeguru.com/cpp/cpp/article.php/c17401/C-Tutorial-PointertoMember-Function.htm http://www.bubuko.com/infodetail-996525.html
參考書目:
C Primer Plus C++ Primer
