簡體   English   中英

這是一個通用函數指針,它是危險的嗎?

[英]Is this a generic function pointer and is it dangerous?

學習並弄亂了函數指針,我注意到了一種初始化void函數指針並轉換它們的方法。 然而,雖然我沒有收到任何警告或錯誤,無論是使用GCC還是VS的編譯器,我都想知道這樣做是危險的還是不好的做法,因為我沒有看到這種方法經常初始化函數指針互聯網。 而且,我們稱之為通用函數指針嗎?

#include <stdio.h>
#include <stdint.h>
#include <conio.h>

#define PAUSE (_getch())

uint16_t add(const uint16_t x, const uint16_t y) {
    return x + y;
}

char chr(uint8_t test) {
    return (char)test;
}

int main(void) {

    void(*test)() = (void*)add;

    const uint16_t x = 1, y = 1;
    uint16_t value = ((uint16_t(*)())test)(x, y);

    test = (void*)chr;

    printf("%d\n", add(x, y));                    // 2
    printf("%d\n", value);                        // 2
    printf("%c\n", ((char(*)())test)(100));       // d

    PAUSE;
    return 0;
}

這是一個通用的函數指針

不,如果我不是非常錯誤,那么在C中就沒有“泛型函數指針”這樣的東西。

這是危險的嗎?

是的。 這是邪惡的。


您需要了解一些事項。 首先,除非您運行的系統符合POSIX,

void(*test)() = (void*)add;

錯的。 void *是指向對象的指針類型,因此它與函數指針不兼容。 (至少在標准C中沒有 - 正如我所提到的,POSIX要求它也與函數指針兼容。)

第二件事是void (*fp)()void (*fp)(void)是不同的。 前一個聲明允許fp采用任意類型的任意數量的參數,並且當編譯器看到對函數(指針)的第一次調用時,將推斷出參數的數量及其類型。

另一個重要的方面是保證函數指針可以相互轉換(AFAIK這表明它們具有相同的表示和對齊要求)。 這意味着任何函數指針都可以分配給任何函數的(地址)(在適當的強制轉換之后),只要你不通過指向不兼容類型的指針調用函數。 當且僅當您在調用指針之前將指針強制轉換回原始類型時,行為才是明確定義的。

所以,如果你想要一個“通用”函數指針,你可以寫一些像

typedef void (*fn_ptr)(void);

然后你可以將任何指向函數的指針fn_ptr類型的對象。 您需要注意的是,在調用函數時,再次轉換為正確的類型,如:

int add(int a, int b);

fn_ptr fp = (fn_ptr)add; // legal
fp(); // WRONG!
int x = ((int (*)(int, int))fp)(1, 2); // good

這里有兩個嚴重的問題:

  1. 從函數指針到對象指針(例如void * )的轉換會觸發未定義的行為:原則上,它可能會使系統崩潰(盡管在實踐中有許多系統可以正常工作)。 而不是void * ,最好為此目的使用函數指針類型。
  2. 你欺騙了編譯器在不知不覺中傳遞一個int以期待的功能uint8_t 這也是未定義的行為,而且非常危險。 由於編譯器不知道它正在這樣做,它甚至不能采取最基本的必要步驟來避免粉碎堆棧 - 你真的在這里賭博。 類似地,這有點微妙,但你欺騙編譯器將兩個int -s傳遞給一個期望兩個uint16_t -s的函數。

還有兩個較小的問題:

  1. 函數指針類型的表示法(例如,在演員表中)是令人困惑的。 我認為最好使用typedef: typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar) typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar)
  2. 調用具有與實際函數不同的簽名的函數指針是未定義的行為。 您可以通過仔細編碼來避免這種情況 - 比您當前的代碼更加謹慎 - 但這很棘手且風險很大。

您可能會使用此方法來破壞調用堆棧,具體取決於調用約定,特別是誰正在進行清理: http//en.wikipedia.org/wiki/X86_calling_conventions對於被調用者清理,編譯器無法知道您有多少變量已經在清理點上傳遞了堆棧,因此傳遞錯誤數量的參數或錯誤大小的參數將終止破壞調用堆棧。

在x64上,每個人都使用調用者清理,因此在這方面你是安全的。 然而,參數值通常是一團糟。 在您的示例中,在x64上,它們將是當時相應寄存器中的任何內容。

C11§6.3.2.3(8)說:

指向一種類型的函數的指針可以被轉換為指向另一種類型的函數的指針並且再次返回; 結果應該等於原始指針。 如果轉換的指針用於調用類型與引用類型不兼容的函數,則行為未定義。

§6.7.6.3(15)說關於兼容的函數類型:

[...]如果一個類型具有參數類型列表而另一個類型由函數聲明符指定,該函數聲明符不是函數定義的一部分並且包含空標識符列表,則參數列表不應具有省略號終止符和類型每個參數應與應用默認參數促銷產生的類型兼容。 [...]

所以,如果你有addchr來獲取int參數(一個int的寬度至少為16位),那就沒問題(如果你沒有將函數指針強制轉換為void * ),但實際上,它是是UB。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM