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

        共 3149字,需瀏覽 7分鐘

         ·

        2020-08-22 05:38

        ?

        本篇的內(nèi)容相對比較簡單 主要從語法的層面講解函數(shù)指針的使用以及應(yīng)用場景。都是些面向入門者的基礎(chǔ),大佬輕噴。

        ?

        首先:什么是函數(shù)指針。

        這個問題老生常談了,不用理解的多么復(fù)雜,它其實就是一個特殊的指針,它用于指向函數(shù)被加載到的內(nèi)存首地址,可用于實現(xiàn)函數(shù)調(diào)用。

        聽上有點像函數(shù)名,函數(shù)名也是記錄了函數(shù)在內(nèi)存中的首地址,加()就可以調(diào)用。

        不錯,不過函數(shù)指針和函數(shù)名還是有點區(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ù)指針都有點抵觸,能不用的時候都盡量不用。

        因為我們印象里見過的函數(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ù)寫成這個鬼樣子。

        不過這也奠定了我們內(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ù)雜點的,帶返回值和參數(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ù)雜一點點,參數(shù)里加個函數(shù)指針?也不是很復(fù)雜,基本只要把函數(shù)名換成(*函數(shù)名)?就可以了
        int?f(int?(*fp)(),int?a?);
        int?(*f_ptr)(int?(*fp)(),int?a?);?
        //?稍微再復(fù)雜一點點,返回值是一個函數(shù)指針:(光是普通函數(shù)返回函數(shù)指針,語法就有點費勁。我們一步一步來:)
        ////?首先搞一個返回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ù)人已經(jīng)懵逼了。
        //?再往復(fù)雜的搞,真就徹底花了,比如返回值和參數(shù)里整上函數(shù)指針數(shù)組,函數(shù)指針參數(shù)里套函數(shù)指針,返回的函數(shù)指針返回值是個函數(shù)指針等等
        //?這種的我們就不研究了。一方面項目中這么寫會挨罵,另一方面太復(fù)雜的我也不會。

        從一開始的void f();,到最后成了這個void (*(*f_ptr)())();鬼樣子

        說真的最后這種寫法我是正向推導(dǎo)過來的,如果是你維護(hù)別人的代碼,上來看到一個這void (*(*f_ptr)())();,恐怕得先罵一會兒娘才能正式開始工作

        然而這卻只是返回函數(shù)指針的函數(shù)指針的最簡單的寫法,參數(shù)全為空,返回全為void,也不涉及指針數(shù)組,還完全沒有進(jìn)行太多反人類的語法變種。

        好在,我們還是有辦法給他整的簡化一點的

        把函數(shù)指針弄成一個自定義類型

        我們把關(guā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ù)指針給搞成一種類型,然后就像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ù)指針的寫法和用法,其實很簡單。

        稍微總結(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)來

        之所以一直不扯,是因為函數(shù)指針和數(shù)組結(jié)合在一起的話,可讀性一下下降了好幾個數(shù)量級

        掌握了上面的寫法,我們再把復(fù)雜度提升億點點:定義一個長度為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大法,可以幫助我們簡化上面這種寫法:(說是簡化,其實寫的更多,但是可讀性更好)

        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ù)組這種搞法確實還是有一定的應(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指針?!?/strong>

        類靜態(tài)成員函數(shù)指針

        類的靜態(tài)成員函數(shù)沒有this指針,它的存儲方式和普通的函數(shù)是一樣的,可以取得的是該函數(shù)在內(nèi)存中的實際地址

        所以靜態(tài)的成員函數(shù)指針的聲明和調(diào)用,和普通函數(shù)指針沒有任何區(qū)別:

        • 聲明:void (*static_fptr)();
        • 調(diào)用:static_fptr();

        唯一有區(qū)別的,就是賦值。因為要傳的是一個類的靜態(tài)成員函數(shù)的地址,所以賦值的時候,要加上類名限定:

        • void (*static_fptr)() = &Test::staticFunc;

        同樣,通過typedef把它搞成類型用法和之前也一樣,可以使代碼更清晰。

        類成員函數(shù)指針

        與靜態(tài)函數(shù)不同,成員函數(shù)在被調(diào)用時,必須要提供this指針。

        因為在它被調(diào)用之前,自己也不知道哪個對象的此函數(shù)被調(diào)用。所以通過&拿到的不是實際的內(nèi)存地址。

        只有調(diào)用的時候,C++才會結(jié)合this指針通過固定的偏移量找到函數(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類的一個實例,通過對象調(diào)用。
          • (pt->*fptr)();,pt是一個指向Test類對象的指針,通過指針調(diào)用。

        C++成員函數(shù)的調(diào)用需要至少3個要素:

        1. this指針;
        2. 函數(shù)參數(shù)(也許為空);
        3. 函數(shù)地址。

        上面的調(diào)用中,->*.*運算符之前的對象指針提供了this(和真正使用this并不完全一致)

        參數(shù)在括號內(nèi)提供,fptr則提供了函數(shù)地址。

        指向虛函數(shù)的函數(shù)指針

        虛函數(shù)其實就是一種特殊的成員函數(shù),所以指向虛函數(shù)的函數(shù)指針寫法,同上。

        不一樣的是:「虛函數(shù)函數(shù)指針同樣具有虛函數(shù)的特性——多態(tài):基類的成員函數(shù)指針可以賦值給繼承類的成員函數(shù)指針?!?/strong>

        另外,指向虛函數(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.

        也可以隨便寫一個驗證一下,編譯報錯也很明確:

        語法總結(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)();必須要加括號

        因為調(diào)用的優(yōu)先級比.*->*高,不加括號就成了:t.*fptr();,這其實相當(dāng)于:t.*(fptr());。

        把后面當(dāng)成一個整體,然而fptr并不是一個函數(shù),編譯會直接失敗。

        ::* .* ->*并不只是針對函數(shù)指針,如果在類外部聲明指向類內(nèi)成員「變量」的指針的話,也要用這幾個操作符才行。

        一個非常簡單的實例

        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)實踐中其實是非常廣泛的。

        網(wǎng)上很多關(guān)于函數(shù)指針的應(yīng)用場景的講解都會自己設(shè)計個場景講解一小段。

        我這里就不班門弄斧了,給大家找?guī)讉€我工作中遇見過的開源項目,看看他們的函數(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)

        dpkgLinux Debian系系統(tǒng)自帶的包管理工具,管理整個系統(tǒng)的安裝包安裝卸載,常見的用法有:

        • dpkg -i 包名dpkg --install 包名安裝
        • dpkg -l 列出所有包詳細(xì)信息
        • dpkg -l 包名 列出指定包詳細(xì)信息
        • dpkg --purge 軟件名 或者dpkg -P 軟件名 卸載軟件
        • 復(fù)雜一點的組合用法:dpkg -D2 --ignore-depends=libgtk --force -i 包名 等等。

        像這種命令工具的邏輯如果讓我寫,指定滿屏幕的if else把自己也繞暈。

        但是在dpkg的源碼里,就用了一種比較高端的玩法

        (其實大多數(shù)命令行工具在解析命令參數(shù)的時候都有用這種辦法,這里我為了好懂一點有所改動,源碼比這個還要晦澀很多,純C的項目屬實有點難啃):

        struct?cmdinfo?{????????????????//?命令結(jié)構(gòu)體,每一種命令對應(yīng)一個實例,存放命令本身的字符串以及執(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?}
        };

        乍一看有點眼暈,沒事,一步一步來:

        ACTIONACTIONBACKEND都是宏,最后他們都變成了一個cmdinfo結(jié)構(gòu)體的定義。所以可以看做和它下面的一樣。

        這段程序為了能實現(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)去)。

        至于解析的具體的實現(xiàn),其實你都不用太關(guān)注細(xì)節(jié),你只需要知道這么寫能實現(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);??????????//?因為只打印信息,阻止dpkg的后續(xù)代碼執(zhí)行,這里直接退出
        }
        //?......?
        static?const?struct?cmdinfo?cmdinfos[]=?{
        ??//?.......
        ??{?"hello",????????????'H',??0,?NULL,??????????NULL,??????hello_world,?0?},?//?新添加的一行,位置只要在結(jié)尾行上面就行
        ??//?.......
        ??{?NULL,????????????????0,???0,?NULL,??????????NULL,??????NULL,??????????0?}
        };

        運行結(jié)果:

        在這里函數(shù)指針就為這種靈活的調(diào)用方式提供了強(qiáng)有力的支持!

        這個功能實現(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ù)雜點的“包含函數(shù)指針的結(jié)構(gòu)體數(shù)組”,我也把他歸為轉(zhuǎn)移表里面了。
        我個人認(rèn)為這么歸類是合理的,但是因為沒找到官方有“轉(zhuǎn)移表”的說法和明確定義,不知道這里這么歸類是否合適。關(guā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)實際發(fā)送的信號)注冊了一個處理函數(shù)ctrl_c_is_pressed

        每當(dāng)程序收到SIGINT信號時,它就會執(zhí)行我們注冊的這個函數(shù)。(如果我們沒有注冊,他會執(zhí)行系統(tǒng)內(nèi)置的默認(rèn)行為,也就是中斷程序)

        我這里說的回調(diào)函數(shù),就是通過函數(shù)指針來實現(xiàn)的,你可以看到我在注冊的時候直接傳了函數(shù)名稱進(jìn)去,并把它和SIGINT信號綁定到了一起。

        然后每當(dāng)程序收到SIGINT信號的時候,他就會調(diào)用我們注冊好的函數(shù)。(回調(diào)回調(diào),就是這個意思)

        其實在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);

        其實就是上面,沒有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ù)名。

        你可以從功能上來理解:我們點擊一個按鈕,就要觸發(fā)某個功能,比如開始游戲,關(guān)閉游戲等等。

        這個功能的觸發(fā)需要兩個要素:「【誰】【做什么事情】」

        所以這里每一個按鈕生成的時候,都需要指定兩個必要的參數(shù),一個是“誰”,另一個就是“做什么”。

        只要你指定過這兩個參數(shù),代碼底層會自動處理,在按鈕被點擊的時候,就讓“誰”執(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)用,而且在編程實踐中的應(yīng)用可以說非常廣泛。

        ?

        這里只簡單說明一下這種跨類回調(diào)的場景下,用到了函數(shù)指針。至于他底層的實現(xiàn)的機(jī)制,詳解的話足夠單拉一篇文章了,這里先留個坑,后期寫好補(bǔ)上。

        ?

        上面看到的是cocos2d-x 2.X版本的寫法,這也是官網(wǎng)上可以下載到的第二代中最新的2.2.6的版本。官方早就已經(jīng)不再維護(hù),不過用作代碼的研讀和學(xué)習(xí)非常有用。

        如果你能看懂我上面的講解就會明白,cocos2d-x 這個版本的代碼可讀性非常好,我感覺非常適合我這種稍微有點基礎(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));

        感覺寫法上差別好像不太大,其實底層的實現(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ù)地址并實現(xiàn)調(diào)用。

        在C++當(dāng)中,語言本身并不提供反射機(jī)制。但是仍然可以通過函數(shù)指針實現(xiàn),在很多C++實現(xiàn)的中間件中都有反射的實現(xiàn),我平時了解到的,使用C++實現(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)用。

        之所以說這么做靈活,是因為字符串足夠靈活。

        比如你寫了十個函數(shù),名字分別是function_1、function_2function_3、function_4.....

        為了實現(xiàn)分別調(diào)用,沒有反射你就需要寫十次調(diào)用或者用轉(zhuǎn)移表

        有了反射,你可以用字符串拼接的方式"function_"+i 拼出函數(shù)名,然后invokeMethod來調(diào)用。

        和上面的cocos2d一樣,這里就先了解一下反射這個函數(shù)指針的應(yīng)用場景就好,就不深入講實現(xiàn)原理了。

        (實在是因為Qt這個invokeMethod的實現(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)大的支持,并且代碼的可讀性也明顯提升。

        計劃將在近期再寫一篇文章對std::function進(jìn)行一個簡單的梳理,會和本篇一樣沒什么難度深度,歡迎關(guān)注。

        最后額外補(bǔ)充一個彩蛋:如果你需要一個聲明函數(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
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
            
            

                      • 色图国产| 国产日韩精品无码去免费专区国产 | 青青草成人在线观看 | 操bb网站| 欧美成a|