函數指針的轉換,調用轉換後的指針的結果如何,請問Visual Studio是如何實現的?

前提是我看了TCPL上的一句話,然後做了測試,不明白VS是如何實現的。

英文版:

a pointer to a function may be converted to a pointer to another function type. Calling the function specified by the converted pointer is implementation-dependent; however, if the converted pointer is reconverted to its original type, the result is identical to the original pointer.

中文版:

指向一個函數的指針可以轉換為指向另一個函數的指針。調用轉換後指針所指的 函數的結果依賴於具體的實現。但是,如果轉換後的指針被重新轉換為原來的類型,則結果與原來的指針一致。

測試1:

#include &

int put_int(void);
float put_float(void);

int main()
{
int (*pf)(void) = put_int;
float (*pf2)(void) = (float (*)(void))pf;

printf("%d, %f", pf(), pf2());

system("pause");
return 0;
}

int put_int(void)
{
return -858993460;
}

// 可以沒有put_float函數,也能編譯執行,結果為 -858993460, -1.#IND00
// 如果有put_float函數,不管返回-1.0還是-2.0,結果依然是 -858993460, -1.#IND00
float put_float(void)
{
return -1.0;
}

可以沒有put_float函數,也能編譯執行,結果為 -858993460, -1.#IND00

如果有put_float函數,不管返回-100還是-2000,結果依然是 -858993460, -1.#IND00

測試2:

#include &

int put_int(void);
float put_float(void);

int main()
{
float (*pf)(void) = put_float;
int (*pf2)(void) = (int (*)(void))pf;

printf("%f, %d", pf(), pf2());
getchar();
return 0;
}

float put_float(void)
{
return -1.0;
}

// 可以沒有put_int函數,也能編譯執行,結果為 -1.000000, -858993460
// 如果有put_int函數,不管返回-123456789還是-987654321,結果依然是 -1.000000, -858993460
int put_int(void)
{
return -123456789;
}

可以沒有put_int函數,也能編譯執行,結果為 -1.000000, -858993460

如果有put_int函數,不管返回-123456789還是-987654321,結果依然是 -1.000000, -858993460

__________________________________

大家可以看出 test2 的%d的列印值與 test1 的有關係,可是我新建了一個解決方案呀,搞不懂。同時我在兩個在線編譯網站上執行 test2 結果分別為:

http://codepad.org: -1.000000, 0

http://ganquan.info:-1.000000, 1


其實這個問題和這個例子很像:

#include &
#include &
using namespace std;
int put_int(void);
float put_float(void);

int main()
{
int (*pf)(void) = put_int;
float (*pf2)(void) = (float (*)(void))pf;

int x = 37;
float f = *(float *)x;

cout &<&< pf2() &<&< endl; cout &<&< f &<&< endl; return 0; } int put_int(void) { return 37; } float put_float(void) { return -1.0; }

原因在於int 和 float 的數字表示不一樣,Floating point

斯坦福大學公開課:編程範式 37分鐘開始


你問這個問題的具體疑問是什麼,一個類型的函數指針不能被另一個類型的函數指針使用,這一點你是已經知道的,然後你問VS是怎麼實現的是怎麼回事呢?

你是不是想知道:僅僅就是返回值不一樣,為什麼VS會實現成這個結果不一樣?

我沒有時間去調查,但可以給一個顯而易見的可能性:編譯器對你的程序進行翻譯,是基於局部的信息的,你在調用函數的地方,給了一個函數類型,說你調用了一個函數,返回值是int,它會在嘗試在eax或者r1上取返回值,如果你反過來,它就有可能在st0之類的地方取返回值了。

你非要改變函數的類型來使用,結果在不同的平台,編譯器,乃至版本上,運行實例上,都是可以不一樣的。

我拿Linux@x86_64試了一下,你可以明顯看到,兩個返回值,一個用的rax, 一個用的xmm0,根本就不是一個值,哪裡有什麼解釋這回事啊。

pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movq $put_int, -16(%rbp)
movq -16(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
call *%rax
cvtss2sd %xmm0, %xmm1
movsd %xmm1, -24(%rbp)
movq -16(%rbp), %rax
call *%rax
movsd -24(%rbp), %xmm0
movl %eax, %esi
movl $.LC0, %edi
movl $1, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret


@專治高級噴子

說的基本符合事實。

就是浮點數返回值,根本不是通過 eax 寄存器傳遞給調用方的。

CPU 上有集成 FPU(浮點單元),你可以把 FPU 理解為一個專用於浮點數運算的獨立 CPU,

FPU 有自己的專用浮點數寄存器(8個),有自己的狀態寄存器,控制寄存器,

這和 CPU 裡面那些寄存器都是彼此獨立的(彼此沒有影響的)。

浮點數都是轉成最高精度的浮點數送到 FPU 里運算的,

運算後的結果可以再轉回普通的浮點數存儲到內存某個位置,

這裡,返回浮點數的時候從 FPU 的某個浮點數寄存器里把數據取出,

並存儲到 stack 上(供 printf 訪問浮點類型的參數)。這個過程根本就繞開了 eax。

所以當你把函數指針轉換了以後,根本就是影響編譯器到什麼地方去取返回值的問題,而不同的函數指針,取返回值的地點是不一樣的啊。

如果是 int,就是 eax。

浮點數,根本就不走 eax。

--

此外,

Calling the function specified by the converted pointer is implementation-dependent;

我認為這句話,是說:(地址是相同的,只是被指向函數的原型不同,在此條件下)取決於編譯器對源代碼編譯出來的結果。

包括:調用方是否清理棧上的參數(平衡 esp),調用方傳遞了哪些參數,以何種方式傳遞給被調用方。

這些屬於調用約定,也就是和 function type 有關。

當然,這樣做很可能會出錯,以至於運行時崩潰,後果由程序員自己負責。


不論如何轉換,最後調用的代碼是不變的,區別在於轉換前後返回值的類型變了,雖然返回值的實際內容並沒有變化。

更新:

雖然返回值並沒有變化,但是返回類型變化後,根據調用規則不同,調用方在不同的寄存器上獲取返回值。

在cdcel中,int型的返回值存儲在eax上,float型的返回值存儲在浮點寄存器ST0上。由此導致強制轉換後返回的float值並不會影響eax寄存器的值。


你的測試1不是沒有put_float也能過,而是有你也根本調用,你的兩個指針都是調用的put_int;你的測試2不是沒有put_int也能過,而是有你也根本調用,你的兩個指針都是調用的put_float。

test2 的%d的列印值與 test1 的完全沒有關係,你想多了。

至於不同編譯器返回的結果不同的問題,你把@藍色和@逍遙嘆的回答綜合看一下


Implementation-dependent 指的是依賴於編譯器的實現,而不是你程序的實現(好坑爹的問題)


前面有人講的 所謂的int 和 float 位元組表示不一樣!根本不是!我想問這個人是怎麼猜的啊? 不知為何這麼多人還頂它?難道這就是學術的魅力?

首先呢,函數指針是c語言層面才有的,編譯成彙編之後,只有函數地址,一個地址,僅此而已,沒有什麼參數,也沒有什麼返回值描述。一切都是棧和寄存器的操作,所以這種問題要看編譯器如何編譯~

在c/c++的編譯中,編譯器會根據不同的環境做N多不同的位元組優化!然而vs的Debug模式不僅不會優化還會添加很多冗餘位元組的調試信息。

簡單分析下test1~

VS2015 test1 Release&>

//main

int(*pf)(void) = put_int;
float(*pf2)(void) = (float(*)(void))pf;

printf("%d, %f", pf(), pf2());
00A21000 call put_int (0A21030h)
00A21005 sub esp,8
00A21008 fstp qword ptr [esp]
00A2100B call put_int (0A21030h)
00A21010 push eax
00A21011 push offset string "%d, %f" (0A22108h)
00A21016 call printf (0A21050h)
00A2101B add esp,10h

return 0;
00A2101E xor eax,eax
}
00A21020 ret

//put_int
return -858993460;
00A21030 mov eax,0CCCCCCCCh
}
00A21035 ret

可以看到,在調用printf的時候,push只入棧了兩個參數!理論上第一個入棧pf2()雖然調用了,但是並不符合put_int的返回!從代碼可以看到,put_int函數的返回是填充到eax裡面的,

00A21000 call put_int (0A21030h)
00A21005 sub esp,8 //開始作用printf棧
00A21008 fstp qword ptr [esp]

然而在這三行代碼里並沒有eax的操作,而是跑去調用FPU及存取去獲取浮點數去了!!然而pf2指向的put_int並沒有操作FPU浮點數寄存器,而單純只是將 -858993460複製到eax寄存器裡面!

後面的pf()倒是非常正確和精簡~

00A2100B call put_int (0A21030h)
00A21010 push eax

接下來看看gcc編譯的~

Dump of assembler code for function main:
7 {
0x0000000000400536 &<+0&>: sub rsp,0x18

8 int(*pf)(void) = put_int;
9 float(*pf2)(void) = (float(*)(void))pf;
10
11 printf("%d, %f", pf(), pf2());
0x000000000040053a &<+4&>: call 0x400530 & 0x000000000040053f &<+9&>: movss DWORD PTR [rsp+0xc],xmm0
0x0000000000400545 &<+15&>: call 0x400530 & 0x000000000040054a &<+20&>: movss xmm0,DWORD PTR [rsp+0xc]
0x0000000000400550 &<+26&>: cvtps2pd xmm0,xmm0
0x0000000000400553 &<+29&>: mov esi,eax
0x0000000000400555 &<+31&>: mov edi,0x400610
0x000000000040055a &<+36&>: mov eax,0x1
0x000000000040055f &<+41&>: call 0x400410 &

12 return 0;
13 }
0x0000000000400564 &<+46&>: mov eax,0x0
0x0000000000400569 &<+51&>: add rsp,0x18
0x000000000040056d &<+55&>: ret
End of assembler dump.

Dump of assembler code for function put_int:
16 {

17 return -858993460;
18
19 }
0x0000000000400530 &<+0&>: mov eax,0xcccccccc
0x0000000000400535 &<+5&>: ret
End of assembler dump.

可以看出,代碼和vs差不多,同樣的,第一個函數pf2()雖然執行了,都是沒有對應的獲取返回值,但是gcc的第一個參數不是跑去FPU了而是跑去獲取MMX寄存器了,但是FPU和mmx貌似是同一個東西~

test2也簡單分析下~,只給了gcc的~

Dump of assembler code for function main:
7 {
0x0000000000400539 &<+0&>: push rbx

8 float (*pf)(void) = put_float;
9 int (*pf2)(void) = (int (*)(void))pf;
10
11 printf("%f, %d", pf(), pf2());
0x000000000040053a &<+1&>: call 0x400530 & 0x000000000040053f &<+6&>: mov ebx,eax
0x0000000000400541 &<+8&>: call 0x400530 & 0x0000000000400546 &<+13&>: unpcklps xmm0,xmm0
0x0000000000400549 &<+16&>: cvtps2pd xmm0,xmm0
0x000000000040054c &<+19&>: mov esi,ebx
0x000000000040054e &<+21&>: mov edi,0x400600
0x0000000000400553 &<+26&>: mov eax,0x1
0x0000000000400558 &<+31&>: call 0x400410 &

12 return 0;
13 }
0x000000000040055d &<+36&>: mov eax,0x0
0x0000000000400562 &<+41&>: pop rbx
0x0000000000400563 &<+42&>: ret

End of assembler dump.

Dump of assembler code for function put_float:
16 {

17 return -1.0;
18 }
0x0000000000400530 &<+0&>: movss xmm0,DWORD PTR [rip+0xd0] # 0x400608
0x0000000000400538 &<+8&>: ret
End of assembler dump.

同樣的,我們看到第一個入棧的參數pf2(),雖然方法call了,但是處理返回結果不對!put_float明明是將數據寫入到xmm寄存器了,

0x000000000040053a &<+1&>: call 0x400530 & 0x000000000040053f &<+6&>: mov ebx,eax

但是處理返回的時候卻是跑去獲取eax,還以為是整形呢!

從以上可以看出,

float(*pf2)(void) &< = &> int(*pf)(void)

這種返回值類型不同的函數指針強制轉換,只會讓編譯器暈頭轉向!!根本摸不著頭腦!!

至於test2返回0或者1,可能是eax寄存器原來的舊值!只是可能,因為不同的編譯器在不同的環境下會編譯出不同的位元組!!


C語言中,函數指針的類型是對於編譯器層面來說的,是編譯器用來檢查語法對不對.在運行時,函數指針都是一個數字,沒什麼類型可說...


函數指針是一個值,你說它是啥它就是啥(強制轉換),按照你指定的方式運算。這個要你知道這個地址能幹啥,胡亂強制轉換程序會掛掉的。


推薦閱讀:

TAG:編程語言 | C編程語言 | MicrosoftVisualStudio | CC | C語言入門 |