C語言學習筆記(七) 函數指針

“函數指針”和“指針函數”是一對容易把人弄暈的概念,但我們只要把握好定語,倒也不難理解。這兩個名詞都是簡稱,“指針函數”是“返回值爲指針的函數”,而“函數指針”則是“指向函數的指針”。這篇主要講講函數指針。

我們講有int 指針,char指針,它們都是一個指針指向這個變量的實際地址。而C語言在編譯函數的時候每個函數會有一個入口地址,當我們用一個指針指向這個入口地址,它就稱爲函數指針。有了這樣一個指針之後,可以通過訪問這個指針來調用該函數。從這個角度看,實際上函數指針和基本類型指針定義是一樣的,只是指向的對象不同而已。那麼,來看看不同的函數指針吧。


新手場:指向函數的簡單指針

int add(int a, int b){
    return a+b;
}
int sub(int a, int b){
    return a-b;
}

int main(int argc, char** argv){
    int (*p)(int, int)=NULL;
    p = add;
    PRTINT( p(2,3) );        //輸出5
    p = sub;
    PRTINT( p(3,1) );        //輸出2
    return 0;
}

說明:上面的PRTINT是一個輸出宏,實際上就是printf函數,只是爲了方便觀察指針的使用。可以看出,函數指針的定義方式是 返回類型 (*指針名)([參數列表])。注意:

  • 函數指針和指向函數的返回值的類型和參數都必須嚴格一致

  • 定義指針的形式還可以寫作 int (*p)(int a, int b); 形參的命名沒有影響;

  • 給指針賦值的形式還可以寫作 p = &add; 兩種方式沒有本質區別。

  • 調用指針的形式還可以寫作 (*p)(2,3)。


進階場:複雜函數指針

*(int*)&p ----這是什麼?

看看這段代碼:

void fun(){
    printf("call fun().\n");
}

int main(int argc, char** argv){
    void (*p)();
    *(int*)&p = (int)&fun;      //效果等價於 p = fun;
    (*p)();
    return 0;
}

雖然註釋裏已經“劇透”了,但是初次看到 *(int *)&p = (int)&fun; 這個表達式的朋友恐怕多半會頭疼。對付這種複雜表達式,別急,一點點去啃,總會看懂的。

首先看看等號右邊。如前所述,函數名本身實際上就可以代表一個地址,&fun也是這個地址。那麼(int)&fun表示把這個地址轉換成int 類型。

再看看前一個表達式: void (*p)();這是一個簡單的函數指針。表示一個返回值爲void、參數列表爲空的函數指針(實際上就是fun的樣式)。

&p表示取指針變量p本身的地址,這是一個32位常數(在32bit系統下)。

(int *)&p表示把取到的這個地址強制轉換成int類型的指針。

那麼 *(int *)&p = (int)&fun; 就是把這個fun()函數的地址賦給p了。所以實際上這行代碼的效果等效於 p = fun; 。

從這個例子也可以看出,函數指針也是指針,完全沒有什麼特殊性,一般指針能做的操作它也能做。


(* (void (*)()) 0)();這又是什麼?

這是《C陷阱與缺陷》裏的一個經典例子。下面就來看看這句代碼什麼意思:

第一步: void (*)().這可以看出是一個函數指針,這種函數的返回值爲void,參數列表也爲空。注意,這個指針沒有名字,也許是最令人困惑的地方。

第二步:(void (*)())0.這是把0強制轉換成這種類型指針。0是一個地址,也就是說這裏假定函數起始地址在0點處。

第三步:(* (void (*)())0)().看得出來這一步與前一步僅僅多出一個(*)().因此,這是函數調用。

雖然層層抽絲最終能看出這行代碼的真面目,但實際上這句代碼是無法直接執行的,不信的話你可以放在main函數裏去試試。這是由於0地址的原因造成的。我們完全可以測試一下,使用以下代碼:

void fun(){
    printf("call fun().\n");
}

int main(int argc, char** argv){
    (* (void (*) () ) 0x401352)();
//    void (*p)() = fun;
//    (*p)();
    return 0;
}

我使用的是CodeBlocks,先使用註釋裏的代碼,並在Watch窗口查看到函數的實際地址值,我這裏是0x401352。那麼把上面代碼中的0替換成這個地址,把後兩句關掉,結果顯示調用到了fun().

wKiom1RcOwugdBewAABc8pFioMo276.jpg

注:在《C陷阱與缺陷》原書中,這句代碼的執行環境是一種微處理器。機器啓動時硬件會調用首地址爲0位置的程序。而這句代碼實際上就是爲了在開發的時候模擬開機啓動時的情形。因此,在我們自己的機器上是無法直接用0地址的。

還有更爲複雜的函數指針形式,這裏不一一列舉,但只要牢記方法就不用怕:先找到核心最像簡單函數指針的部分,再一點點展開慢慢分析。這就好比孫猴子的火眼金睛,任你小妖精怎麼換馬甲,總會被咱看出破綻,是不是?


函數指針數組

現在我們可以看穿函數指針的馬甲了,比如下面這個就是一個函數指針:

char * (*pf)(char *p);

考慮數組的概念,把相同類型的元素放在一起就可以組成數組。那麼我們把函數指針放在一個數組裏是不是就成了下面這樣:

char *(*pf[3])(char *p);

這……星號好多呀……嗯,它是一個長度爲3的數組。數組的每個元素都是一個函數指針。每個函數指針指向的函數返回值類型都是char*,形參列表都只有一個char*參數。喏,這樣就理順了,最重要的是它是一個數組。下面的代碼演示了這種數組的使用:

char * fun1(char *p){
    printf("%s\n", p);
    return p;
}
char * fun2(char *p){
    printf("%s\n", p);
    return p;
}

int main(int argc, char **argv){
    char* (*pa[2])(char*);
    pa[0] = fun1;    //可以直接用函數名
    pa[1] = &fun2;    //也可以加上&符號

    pa[0]("fun1");
    pa[1]("fun2");
    return 0;
}


指向函數指針數組的指針

這個更拗口了。不過,只要我們瞭解了函數指針數組,則指向函數指針數組的指針也不是不能理解。函數指針數組的指針用起來跟其他指針沒什麼區別,只不過調試難度可能會增加。下面的代碼定義了一個指向函數指針數組的指針,有興趣的同學可以看看,這裏就不贅述了。

char * (*(*pf)[3])(char *p);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章