本篇的內(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();
編譯器會認為這是一個 返回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ù)雜一些。
這里循序漸進地舉幾個相對復(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)過來的,如果是你維護別人的代碼,上來看到一個這void (*(*f_ptr)())();
,恐怕得先罵一會兒娘才能正式開始工作
然而這卻只是返回函數(shù)指針的函數(shù)指針
的最簡單的寫法,參數(shù)全為空,返回全為void
,也不涉及指針數(shù)組,還完全沒有進行太多反人類的語法變種。
好在,我們還是有辦法給他整的簡化一點的
把函數(shù)指針弄成一個自定義類型
我們把關(guān)注點聚焦到上面最后一個函數(shù)指針上,定義一個返回值是函數(shù)指針的函數(shù),完整的聲明加調(diào)用應(yīng)該是這樣的:
#include <iostream>
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 <iostream>
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ù)組扯進來
之所以一直不扯,是因為函數(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個要素:
-
this指針; -
函數(shù)參數(shù)(也許為空); -
函數(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ù)指針在涉及到多繼承和指針強轉(zhuǎn)的問題時,使用不當(dāng)會踩到大坑:
-
不要使用 static_cast
將繼承類的成員函數(shù)指針賦值給基類成員函數(shù)指針,如果一定要使用,首先確定沒有問題。(這條可能會限制代碼的可擴展性。) -
如果一定要使用 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.
也可以隨便寫一個驗證一下,編譯報錯也很明確:
語法總結(jié)
類函數(shù)指針的語法相當(dāng)嚴格:
對于類內(nèi)成員的函數(shù)指針的使用和獲取,要注意的是:
-
不能使用括號:例如 &(ClassName::foo)
不對。 -
必須有限定符:例如 &foo
不對。即使在類ClassName
的作用域內(nèi)也不行。 -
必須使用取地址符號:例如直接寫 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é)一定都用敲很多命令,有些命令行工具特別強大,比如像什么sed
,awk
等等。
這些工具無一例都可以對復(fù)雜的命令行參數(shù)進行精準(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
列出所有包詳細信息 -
dpkg -l 包名
列出指定包詳細信息 -
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 }
};
乍一看有點眼暈,沒事,一步一步來:
ACTION
和ACTIONBACKEND
都是宏,最后他們都變成了一個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ù)名進去)。
至于解析的具體的實現(xiàn),其實你都不用太關(guān)注細節(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ù)組里就可以
解析是全自動而且可靈活擴展的,你根本不需要知道太多細節(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)用方式提供了強有力的支持!
這個功能實現(xiàn)的核心,就是在結(jié)構(gòu)體里存放了一個函數(shù)指針變量。
在代碼執(zhí)行的時候,通過匹配到不同的參數(shù),就找不同的函數(shù)調(diào)用來執(zhí)行不同的功能。
相比于寫if
else
switch
case
,這種寫法不僅高端而且靈活高效,擴展性又非常好,而且還很簡潔易讀(對于有一定基礎(chǔ)的同學(xué)而言)
很多網(wǎng)上的資料對于轉(zhuǎn)移表的講解,都是一個單純的函數(shù)指針數(shù)組,這里是一個相對復(fù)雜點的“包含函數(shù)指針的結(jié)構(gòu)體數(shù)組”,我也把他歸為轉(zhuǎn)移表里面了。
我個人認為這么歸類是合理的,但是因為沒找到官方有“轉(zhuǎn)移表”的說法和明確定義,不知道這里這么歸類是否合適。關(guān)于這一點歡迎感興趣的小伙伴調(diào)研補充。
應(yīng)用場景二、回調(diào)函數(shù)
二.1 函數(shù)指針回調(diào)
linux系統(tǒng)編程中,可以使用signal
函數(shù)讓程序具備處理內(nèi)置系統(tǒng)信號的能力。
比如像這樣一個程序(linux上玩,windows編不過哦):
#include <iostream>
#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)置的默認行為,也就是中斷程序)
我這里說的回調(diào)函數(shù),就是通過函數(shù)指針來實現(xiàn)的,你可以看到我在注冊的時候直接傳了函數(shù)名稱進去,并把它和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
拋去你不認識的部分,只看函數(shù)聲明:__sighandler_t signal (int __sig, __sighandler_t __handler);
這個__sighandler_t
你再往下挖就會驚喜的發(fā)現(xiàn):
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);
這下認識了吧,signal
就是一個返回函數(shù)指針的函數(shù),他還包含兩個參數(shù),一個是int,另一個是函數(shù)指針。
這個函數(shù)指針可以指向一個參數(shù)為int,返回為空的函數(shù),所以我們上面寫的ctrl_c_is_pressed
可以直接傳進去
在很多文章里或者有些舊版的代碼里寫的都是這樣的:
void (*signal(int signo, void (*func)(int)))(int);
其實就是上面,沒有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ù)名。
你可以從功能上來理解:我們點擊一個按鈕,就要觸發(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)機制至少要傳兩個參數(shù),一個是函數(shù)地址,一個是this
指針。
這種跨類回調(diào)也是函數(shù)指針的一個經(jīng)典應(yīng)用,而且在編程實踐中的應(yīng)用可以說非常廣泛。
這里只簡單說明一下這種跨類回調(diào)的場景下,用到了函數(shù)指針。至于他底層的實現(xiàn)的機制,詳解的話足夠單拉一篇文章了,這里先留個坑,后期寫好補上。
上面看到的是cocos2d-x 2.X版本的寫法,這也是官網(wǎng)上可以下載到的第二代中最新的2.2.6的版本。官方早就已經(jīng)不再維護,不過用作代碼的研讀和學(xué)習(xí)非常有用。
如果你能看懂我上面的講解就會明白,cocos2d-x 這個版本的代碼可讀性非常好,我感覺非常適合我這種稍微有點基礎(chǔ)的初學(xué)者學(xué)習(xí)。
到了3.x版本里(我下的3.17.2),這種跨類的回調(diào)機制玩法也早已換成了風(fēng)騷萬倍的C++11的玩法:
auto closeItem = MenuItemImage::create(
"CloseNormal.png",
"CloseSelected.png",
CC_CALLBACK_1(HelloWorld::menuCloseCallback,this));
感覺寫法上差別好像不太大,其實底層的實現(xiàn)完全換了一種機制。上面2.X版本,使用的跨類函數(shù)指針進行回調(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的反射機制,可以直接把字符串映射成真正的函數(shù)地址并實現(xiàn)調(diào)用。
在C++當(dāng)中,語言本身并不提供反射機制。但是仍然可以通過函數(shù)指針實現(xiàn),在很多C++實現(xiàn)的中間件中都有反射的實現(xiàn),我平時了解到的,使用C++實現(xiàn)的最完善的動態(tài)反射機制當(dāng)屬Q(mào)t的QMetaObject::invokeMethod();
反射最大的好處,就是讓你的代碼一般人輕易看不懂,IDE里Ctrl+鼠標(biāo)左鍵跳轉(zhuǎn)不過去。
維護難度一上來,你的價值就體現(xiàn)出來了,等待你的將是升職加薪,迎娶白富培走向人生....扯遠了。
反射最大的好處,是讓你的代碼靈活度和可擴展性大大提升。不過相對的,可維護性也有一定的損失。
有了反射之后,你完全可以通過QMetaObject::invokeMethod("function_name");
來進行函數(shù)調(diào)用。
之所以說這么做靈活,是因為字符串足夠靈活。
比如你寫了十個函數(shù),名字分別是function_1
、function_2
、function_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)機制啃了一次不得要領(lǐng),就不敢深入瞎講了。)
最后
以上就是本篇關(guān)于C++函數(shù)指針講解的全部內(nèi)容,一篇典型收藏吃灰系列的文章
就是簡單捋了一下函數(shù)指針的寫法、功能以及應(yīng)用
沒什么深度,所以應(yīng)該也沒什么嚴重的誤導(dǎo)和錯誤
上面提到了在cocos2d-x的新版本中用std::function
代替了函數(shù)指針,這也是現(xiàn)在C++框架和應(yīng)用的主流寫法
C++11提供的std::function
將從語法層面為函數(shù)指針的使用提供強大的支持,并且代碼的可讀性也明顯提升。
計劃將在近期再寫一篇文章對std::function
進行一個簡單的梳理,會和本篇一樣沒什么難度深度,歡迎關(guān)注。
最后額外補充一個彩蛋:如果你需要一個聲明函數(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
點【在看】是最大的支持
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!