C++ 函數指針取地址與取值

什麼是函數指針?

void (*funptr)(int param);

這就是一個簡單的函數指針的聲明。顧名思義,函數指針是一個特殊的指針,它用於指向函數被加載到的內存首地址,可用於實現函數調用。數組

函數名也是指向函數的內存首地址的,他和函數指針有什麼不一樣?——既然他是指針,並且不是const的,那麼他就是靈活可變的,經過賦值不一樣的函數來實現不一樣的函數調用。函數

然而他也有本身的限制(函數簽名——返回值類型和參數類型),那不是和覆蓋、多態實現的功能同樣了麼?額。。。要這麼理解也行,但不全對。編碼

函數指針做用

上面說到函數指針的功能相似覆蓋或多態,覆蓋和多態更多體現的是對象自身的特徵和對象之間的繼承關聯,而函數指針則沒這麼多講究,他就是靈活。spa

函數指針不須要依附於對象存在,他能夠用來解決基於條件的多個函數篩選,也能夠處理徹底無關的幾個函數。指針

因此他的做用,什麼封裝性好、用於回調函數、實現多態等等,隨便了,只有一條,符合他的函數簽名而且可達。調試

如何使用

和變量指針相似,聲明->賦值->使用 或者 定義->使用code

int add(int a,int b)
{
    return a + b;
}

//1.聲明->賦值->使用
int (*fun)(int a,int b);
fun = add;    //也可以使用 fun = &add;從某篇文章看到是歷史緣由,後面稍做分析
fun(5,10);    //也可以使用 (*fun)(5,10);緣由同上
//2.經過宏定義函數指針類型 #define int (*FUN)(int,int); FUN fun = NULL; fun = add; fun(5,10); //3.定義->使用 int (*fun)(int a,int b) = add; fun(5,10); //4.宏定義 #define int (*FUN)(int,int); FUN fun = add; fun(5,10);

函數指針的取地址、取值

上面的代碼中,又是取地址符&,又是取引用符*,結果還能相互賦值,交叉調用,這又怎麼理解?對象

首先來看下函數指針、函數名的類型。對於函數指針fun,類型爲 int (*)(int,int),這個很好理解,函數名add的類型,經過VS在靜態狀況下用鼠標查看,類型爲int (*)(int,int)。blog

what?爲啥不是int ()(int,int)呢?這個咱們能夠類比數組。數組名爲指向內存中數組首地址的指針,同時數組名可當作指針對數組進行操做。而對於函數名,經過在VS下查看彙編代碼能夠知道,編譯器將函數名賦值爲函數加載入內存的首地址,經過call 函數名來跳轉到相應內存地址進行函數的執行。因此對這裏的函數名爲指針類型也能夠理解。繼承

這樣 fun = add 咱們能夠理解了,那 fun = &add 又是什麼鬼? (*fun)(5,10)也能夠理解,fun(5,10)呢?下面來看看彙編代碼

 

//原始代碼
int main()
{
    FUN f = NULL;
    f = &add;
    FUN f1 = NULL;
    f1 = add;
    add(1,2);

    std::cout<< add <<"  "<< &add <<"   " << *add <<std::endl;
    std::cout<< f <<"   " << &f <<"   " << *f <<"   "<<std::endl;
    std::cout<< (*f)(5, 10) << "   " << f(5,10)  <<std::endl;
    std::cout << (*add)(5, 10) << "   " <<(&add)(5, 10) << std::endl;
}

//對應彙編代碼
    //對add函數做了一層跳轉(從標識add->add函數),記錄了add函數的入口地址
add:
000412F8  jmp         add (042180h)
    //add函數彙編代碼
    int add(int a, int b)
{
00042180  push        ebp  
00042181  mov         ebp,esp  
00042183  sub         esp,0C0h  
00042189  push        ebx  
0004218A  push        esi  
0004218B  push        edi  
0004218C  lea         edi,[ebp-0C0h]  
00042192  mov         ecx,30h  
00042197  mov         eax,0CCCCCCCCh  
0004219C  rep stos    dword ptr es:[edi]  
    return a + b;
0004219E  mov         eax,dword ptr [a]  
000421A1  add         eax,dword ptr [b]  
}

int main()
{
00042450  push        ebp  
00042451  mov         ebp,esp  
00042453  sub         esp,0DCh  
00042459  push        ebx  
0004245A  push        esi  
0004245B  push        edi  
0004245C  lea         edi,[ebp-0DCh]  
00042462  mov         ecx,37h  
00042467  mov         eax,0CCCCCCCCh  
0004246C  rep stos    dword ptr es:[edi]
    //能夠看到,對編譯器來講&add和add實際上是同樣的,都對應內存中的標識add,即上面的jmp代碼的地址
    //這裏f、f1都被看成指針,指向標識add,與 int(*)(int,int) 中的那個*對應
    //由於在同一代碼段,經過offset獲取段內偏移便可實現跳轉
    FUN f = NULL;
0004246E  mov         dword ptr [f],0  
    f = &add;
00042475  mov         dword ptr [f],offset add (0412F8h)  
    FUN f1 = NULL;
0004247C  mov         dword ptr [f1],0  
    f1 = add;
00042483  mov         dword ptr [f1],offset add (0412F8h)  
    //經過call調用函數,函數對應標識add
    add(1,2);
0004248A  push        2  
0004248C  push        1  
0004248E  call        add (0412F8h)  
00042493  add         esp,8  
    //這裏能夠看到,對於add、&add、*add 的值,編譯器都當作標識add,由此能夠猜想,對於函數名編譯器有特殊處理
    //其實這個也能夠理解,首先函數名即函數的內存首地址,對這個地址值取地址沒有什麼意義,函數調度由系統完成,而指針的指針做用就是改變第一層指針的值,改變函數名的指向沒有意義
    //而對其取值,那就是代碼的機器碼,由於代碼存放於只讀段,咱們不會也不能對其進行重寫、拷貝等操做,機器碼咱們也沒法操做,因此這個也沒有意義
    std::cout<< add <<"  "<< &add <<"   " << *add <<std::endl;
00042496  mov         esi,esp 
    //cout爲從右往左,所有壓棧後再先進後出地輸出,第一個爲std::endl
00042498  push        offset std::endl<char,std::char_traits<char> > (041410h)  
0004249D  mov         edi,esp  
0004249F  push        offset add (0412F8h)  
000424A4  push        offset string "   " (049B30h)  
000424A9  mov         ebx,esp  
000424AB  push        offset add (0412F8h)  
000424B0  push        offset string "  " (049B34h)  
000424B5  mov         eax,esp  
000424B7  push        offset add (0412F8h)  
000424BC  mov         ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (04D098h)]  
000424C2  mov         dword ptr [ebp-0DCh],eax  
000424C8  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A4h)]  
000424CE  mov         ecx,dword ptr [ebp-0DCh]
    //函數調用結束後的平衡堆棧檢測
000424D4  cmp         ecx,esp  
000424D6  call        __RTC_CheckEsp (04115Eh)  
000424DB  push        eax  
000424DC  call        std::operator<<<std::char_traits<char> > (041438h)  
000424E1  add         esp,8  
000424E4  mov         ecx,eax  
000424E6  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A4h)]  
000424EC  cmp         ebx,esp  
000424EE  call        __RTC_CheckEsp (04115Eh)  
000424F3  push        eax  
000424F4  call        std::operator<<<std::char_traits<char> > (041438h)  
000424F9  add         esp,8  
000424FC  mov         ecx,eax  
000424FE  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A4h)]  
00042504  cmp         edi,esp  
00042506  call        __RTC_CheckEsp (04115Eh)  
0004250B  mov         ecx,eax  
0004250D  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0ACh)]  
00042513  cmp         esi,esp  
00042515  call        __RTC_CheckEsp (04115Eh) 
    //函數指針f、指針取值*f 都是 dword ptr [f],即標識指針f指向的內存的值(標識add的偏移),若是是取值,*f與*add同樣沒有意義
    //而&f即標識指針f的值,這裏取地址的意義即對應他的指針類型,能夠指向不一樣內存地址來調用不一樣函數
    //由此能夠猜想,對於函數指針編譯器也有相似的特殊處理
    std::cout<< f <<"   " << &f <<"   " << *f <<"   "<<std::endl;
0004251A  mov         esi,esp  
0004251C  push        offset std::endl<char,std::char_traits<char> > (041410h)  
00042521  push        offset string "   " (049B30h)  
00042526  mov         edi,esp  
00042528  mov         eax,dword ptr [f]  
0004252B  push        eax  
    std::cout<< f <<"   " << &f <<"   " << *f <<"   "<<std::endl;
0004252C  push        offset string "   " (049B30h)  
00042531  mov         ebx,esp  
00042533  lea         ecx,[f]  
00042536  push        ecx  
00042537  push        offset string "   " (049B30h)  
0004253C  mov         eax,esp  
0004253E  mov         edx,dword ptr [f]  
00042541  push        edx  
00042542  mov         ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (04D098h)]  
00042548  mov         dword ptr [ebp-0DCh],eax  
0004254E  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A4h)]  
00042554  mov         ecx,dword ptr [ebp-0DCh]  
0004255A  cmp         ecx,esp  
0004255C  call        __RTC_CheckEsp (04115Eh)  
00042561  push        eax  
00042562  call        std::operator<<<std::char_traits<char> > (041438h)  
00042567  add         esp,8  
0004256A  mov         ecx,eax  
0004256C  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A4h)]  
00042572  cmp         ebx,esp  
00042574  call        __RTC_CheckEsp (04115Eh)  
00042579  push        eax  
0004257A  call        std::operator<<<std::char_traits<char> > (041438h)  
0004257F  add         esp,8  
00042582  mov         ecx,eax  
00042584  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A4h)]  
0004258A  cmp         edi,esp  
0004258C  call        __RTC_CheckEsp (04115Eh)  
00042591  push        eax  
00042592  call        std::operator<<<std::char_traits<char> > (041438h)  
00042597  add         esp,8  
    std::cout<< f <<"   " << &f <<"   " << *f <<"   "<<std::endl;
0004259A  mov         ecx,eax  
0004259C  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0ACh)]  
000425A2  cmp         esi,esp  
000425A4  call        __RTC_CheckEsp (04115Eh)  
    //函數調用時 f、*f 都被當成 call dword ptr [f],同上,而&f對應的是指針,就不能進行函數調用了
    std::cout<< (*f)(5, 10) << "   " << f(5,10)  <<std::endl;
000425A9  mov         esi,esp  
000425AB  push        offset std::endl<char,std::char_traits<char> > (041410h)  
000425B0  mov         edi,esp  
000425B2  push        0Ah  
000425B4  push        5  
000425B6  call        dword ptr [f]  
000425B9  add         esp,8  
000425BC  cmp         edi,esp  
000425BE  call        __RTC_CheckEsp (04115Eh)  
000425C3  mov         edi,esp  
000425C5  push        eax  
000425C6  push        offset string "   " (049B30h)  
000425CB  mov         ebx,esp  
000425CD  push        0Ah  
000425CF  push        5  
000425D1  call        dword ptr [f]  
000425D4  add         esp,8  
000425D7  cmp         ebx,esp  
000425D9  call        __RTC_CheckEsp (04115Eh)  
000425DE  mov         ebx,esp  
000425E0  push        eax  
000425E1  mov         ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (04D098h)]  
000425E7  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A8h)]  
000425ED  cmp         ebx,esp  
000425EF  call        __RTC_CheckEsp (04115Eh)  
000425F4  push        eax  
000425F5  call        std::operator<<<std::char_traits<char> > (041438h)  
000425FA  add         esp,8  
000425FD  mov         ecx,eax  
000425FF  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A8h)]  
00042605  cmp         edi,esp  
00042607  call        __RTC_CheckEsp (04115Eh)  
0004260C  mov         ecx,eax  
0004260E  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0ACh)]  
00042614  cmp         esi,esp  
00042616  call        __RTC_CheckEsp (04115Eh) 
    //如上所說,*add、&add、*add三者相同,因此下面兩個調用也成立,可是爲了良好的編碼風格,儘可能避免
    std::cout << (*add)(5, 10) << "   " <<(&add)(5, 10) << std::endl;
0004261B  mov         esi,esp  
0004261D  push        offset std::endl<char,std::char_traits<char> > (041410h)  
00042622  push        0Ah  
00042624  push        5  
00042626  call        add (0412F8h)  
0004262B  add         esp,8  
0004262E  mov         edi,esp  
00042630  push        eax  
00042631  push        offset string "   " (049B30h)  
00042636  push        0Ah  
00042638  push        5  
0004263A  call        add (0412F8h)  
0004263F  add         esp,8  
00042642  mov         ebx,esp  
00042644  push        eax  
00042645  mov         ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (04D098h)]  
0004264B  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A8h)]  
00042651  cmp         ebx,esp  
00042653  call        __RTC_CheckEsp (04115Eh)  
00042658  push        eax  
00042659  call        std::operator<<<std::char_traits<char> > (041438h)  
0004265E  add         esp,8  
00042661  mov         ecx,eax  
00042663  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0A8h)]  
00042669  cmp         edi,esp  
0004266B  call        __RTC_CheckEsp (04115Eh)  
00042670  mov         ecx,eax  
00042672  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (04D0ACh)]  
00042678  cmp         esi,esp  
0004267A  call        __RTC_CheckEsp (04115Eh)  
}

 

綜上所述,對於函數名add,add、*add、&add三者等價,都指向函數首地址;對於函數指針f,f 和 *f 等價,一樣是指向函數首地址;而&f 爲函數指針的地址,由於函數指針 f 的值可變,可指向不一樣的函數。

當咱們調試代碼時,將指針移到函數名或函數指針上,會顯示其類型爲 functionprt,編譯器對其的特殊處理可能就源自於此。

網上有些文章對這種機制的解釋爲,出於歷史的緣由(從面向過程到面向對象的過渡,函數指針與對象指針的關係等等)

相關文章
相關標籤/搜索