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,编译器对其的特殊处理可能就源自于此。

网上有些文章对这种机制的解释为,出于历史的原因(从面向过程到面向对象的过渡,函数指针与对象指针的关系等等)

 

posted @ 2017-12-04 16:09  九鼎煮鸡蛋  阅读(19520)  评论(0编辑  收藏  举报