<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          萬字長文系統(tǒng)梳理C++函數(shù)指針

          共 3149字,需瀏覽 7分鐘

           ·

          2020-08-22 05:38

          ?

          本篇的內(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個要素:

          1. this指針;
          2. 函數(shù)參數(shù)(也許為空);
          3. 函數(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)會踩到大坑:

          1. 不要使用static_cast將繼承類的成員函數(shù)指針賦值給基類成員函數(shù)指針,如果一定要使用,首先確定沒有問題。(這條可能會限制代碼的可擴(kuò)展性。)
          2. 如果一定要使用static_cast, 注意不要使用多繼承。
          3. 如果一定要使用多繼承的話,不要把一個基類的成員函數(shù)指針賦值給另一個基類的函數(shù)指針。
          4. 單繼承要么全部不使用虛函數(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ù)指針的使用和獲取,要注意的是:

          1. 不能使用括號:例如&(ClassName::foo)不對。
          2. 必須有限定符:例如&foo不對。即使在類ClassName的作用域內(nèi)也不行。
          3. 必須使用取地址符號:例如直接寫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)大,比如像什么sedawk等等。

          這些工具無一例都可以對復(fù)雜的命令行參數(shù)進(jìn)行精準(zhǔn)解析。

          如果你自己寫過命令行解析的程序就會發(fā)現(xiàn)這并不是一件容易的事情。

          我在研究多線程打包的時候有看過dpkg的源碼。這里可以簡單講一下:(代碼來源:https://git.dpkg.org/git/dpkg/dpkg.git)

          dpkgLinux 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)眼暈,沒事,一步一步來:

          ACTIONACTIONBACKEND都是宏,最后他們都變成了一個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é)的時候多多少少玩過cocos2dunity3d之類的做過小游戲。

          這里簡單拉出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_1function_2function_3function_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


          瀏覽 109
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄片叉蛋的视频在线播放免费看 | 69久久久久久久 | 欧美日韩免费在线视频 | 青草久久久久 | 91左爱在线 |