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


什么是函數指針?

void (*funptr)(int param);

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

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

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

函數指針作用

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

函數指針不需要依附於對象存在,他可以用來解決基於條件的多個函數篩選,也可以處理完全無關的幾個函數。

所以他的作用,什么封裝性好、用於回調函數、實現多態等等,隨便了,只有一條,符合他的函數簽名並且可達。

如何使用

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

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)。

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,編譯器對其的特殊處理可能就源自於此。

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM