函數指針
一、函數指針的值
函數指針跟普通指針一樣,存的也是一個內存地址, 只是這個地址是一個函數的起始地址, 下面這個程序打印出一個函數指針的值(func1.c):
#include <stdio.h>
typedef int (*Func)(int);
int Double(int a)
{
return (a + a);
}
int main()
{
Func p = Double;
printf("%p\n", p);
return 0;
}
編譯、運行程序:
[lqy@localhost notlong]$ gcc -O2 -o func1 func1.c
[lqy@localhost notlong]$ ./func1
0x80483d0
[lqy@localhost notlong]$
` `然後我們用 nm 工具查看一下 Double 的地址,
看是不是正好是 0x80483d0:
[lqy@localhost notlong]$ nm func1 | sort
08048294 T _init
08048310 T _start
08048340 t __do_global_dtors_aux
080483a0 t frame_dummy
080483d0 T Double
080483e0 T main
...
不出意料,Double 的起始地址果然是 0x080483d0。
二、調用函數指針指向的函數
直接調用一個函數是 call 一個常量, 而通過函數指針調用一個函數顯然不能這麼做, 因為函數地址是可變的了,指向誰就得 call 誰。 下面比較一下直接調用和通過函數指針間接調用同一個函數的 彙編代碼(func2.c):
#include <stdio.h>
typedef int (*Func)(int);
int Double(int a)
{
return (a + a);
}
int main()
{
Func p = Double;
Double(2); // 直接調用
p(2); // 間接調用
return 0;
}
部分彙編代碼如下:
movl $2, (%esp)
call Double
movl $2, (%esp)
movl 28(%esp), %eax # 28(%esp) 是 p
call *%eax
可見通過函數指針間接調用一個函數, call 指令的操作數不再是一個常量, 而是寄存器 eax(其它寄存器應該也行), 此時 eax 寄存器的值正好是 Double 函數的起始地址, 所以接著就會去執行 Double 函數的指令。
三、參數弱匹配
從上面的例子中我們也看到了函數指針也沒什麼特別的, 也就存了個地址,但是調用一個函數不僅需要知道它的起始地址, 還得根據它的參數列表來壓棧傳遞參數。
參數列表在定義函數指針類型的時候就約定好了, 凡是具有相同參數列表的函數都可以賦值給該類型的函數指針, 而參數列表不同的函數也可以通過強制類型轉換後賦值給它 (C語言的指針類型可以任意轉換⊙﹏⊙), 下面這個程序就大膽的強制轉換了一下(func3.c):
#include <stdio.h>
typedef int (*Func)(int);
int Double2(int a, int b)
{
return (a + a);
}
int main()
{
Func p = (Func)Double2;
printf("%d\n", p(2));
return 0;
}
不強制轉換的話,編譯的時候會報告一個 warring (居然不是 error ⊙﹏⊙), 上面這個程序編譯的時候 0 error 0 warring, 執行也沒有出錯:
[lqy@localhost notlong]$ gcc -o func3 func3.c
[lqy@localhost notlong]$ ./func3
4
[lqy@localhost notlong]$
真算是朵奇葩了!
沒有出錯的原因是:參數 a 對應的剛好是壓棧的 2, 而 b 對應的是一個危險地帶,還好沒用到 b, 所以這個程序依然順利地執行完了。
綜上所述,函數指針真沒什麼特別的。