天天看點

函數指針和指針函數的詳細解析

                                                                                                              厘清函數指針和指針函數

關于指針和數組斬不斷理還亂的恩怨還真是說了不少,不過現在應該已經理清了。有了上一講的基礎,本講的内容相對來說就比較容易了解了。

1.指向函數的指針(函數指針)

來分析這樣一個聲明,void (*f) ( );雖然()的優先級高于*,但由于有括号存在,首先執行的是解引用,是以f是一個指針;接下來執行( ),表明f指向一個函數,這個函數不傳回任何值。現在得出結論:f是一個指向不接受參數且不傳回任何值的函數的指針,簡稱函數指針(pointer to function)。

對比一下int(*p) [100],p是一個指向含有100個整型元素的數組的指針,它們有一個共同的特點:指針聲明符(*)和辨別符(f或p)都被限制在一個括号中,由于括号的優先級是最高的,是以我們從辨別符開始由内向外分析,即可得到以上結果。

<1>.初始化

注意指向函數的指針(函數指針)指向的是函數而非普通的變量,它所指向的函數也是有特定類型的,函數的類型由它的傳回值類型以及形參清單确定,和函數名無關。對函數指針初始化時可以采用相同類型函數的函數名或函數指針(當然還有零指針常量)。假如有函數void test ( ),int wrong_match (int)和函數指針void (*ptf) ( )。

下面的初始化是錯誤的,因為函數指針的類型與函數的類型不比對:

f = wrong_match;

f = & wrong_match;

ptf = wrong_match;

ptf = & wrong_match;

以下初始化及指派是合法的:

f = test;

f = &test;

ptf = test;

ptf = &test;

f = pf;

要做出解釋的是test和&test都可以用來初始化函數指針。C語言規定函數名會被轉換為指向這個函數的指針,除非這個函數名作為& 操作符或sizeof操作符的操作數(注意:函數名用于sizeof的操作數是非法的)。也就是說f = test;中test被自動轉換為&test,而f= &test;中已經顯示使用了&test,是以test就不會再發生轉換了。是以直接引用函數名等效于在函數名上應用 & 運算符,兩種方法都會得到指向該函數的指針。

<2>.通過函數指針調用函數

通過函數指針調用函數可以有兩種方法,直接使用函數指針或在函數指針前使用解引用運算符,如下所示:

f = test;

ptf = test;

f ( );

(*f) ( );  //指針兩側的括号非常重要,表示先對f解引用,然後再調用相應的函數

ptf ( );

(*ptf) ( ); //括号同樣不能少

以上語句都能達到調用test函數的作用。ANSI C标準将f ( )認為是(*f)( )的簡寫形式,并且推薦使用f ( )形式,因為它更符合函數調用的邏輯。要注意的是:如果指向函數的指針沒有初始化,或者具有0值(零指針常量),那麼該指針不能在函數調用中使用。隻有當指針已經初始化,或被指派後指向某個函數才能安全地用來調用函數。

<3>.探究函數名

現在有如下程式:

#include<stdio.h>

void test( )

{

    printf("test called!\n");

}

int main()

{

   void (*f) ( );

    f = test;

   f ( );

    (*f)( );

    //test++;             // error,标準禁止對指向函數的指針進行自增運算

          //test = test + 2;        //error,不能對函數名指派,函數名也不能用于進行算術運算

          printf("%p\n", test);

          printf("%p\n", &test);

          printf("%p\n", *test);

  return 0;

}

在我機器上的運作結果為:

test called!

test called!

004013EE

004013EE

004013EE

這個程式中較難了解的是3個輸出語句都可以得到函數的入口位址。首先來看函數名test,它與數組名類似(注意:隻是類似),是一個符号用來辨別一個函數的入口位址,在使用中函數名會被轉換為指向這個函數的指針,指針的值就是函數的入口位址,&test在前面已經說了:顯示擷取函數的位址。對于*test,可以認為由于test已經被轉換成了函數指針,指向這個函數,是以*test就是取這個指針所指向的函數名,而又根據函數名會被轉換指向該函數的指針的規則,這個函數也轉變成了一個指針,是以*test最終也是一個指向函數test的指針。對它們采用%p格式項輸出,都會得到以16進制數表示的函數test的入口位址。注意函數的位址在編譯期是未知的,而是在連結時确定的。

2.傳回指針的函數(指針函數)

類比指針數組(還記得嗎),了解指針函數将會更加輕松。所謂指針函數,就是傳回指針的函數,函數可以不傳回任何值,也可以傳回整型值,實型值,字元型值,當然也可以傳回指針值。一個指針函數的聲明:int *f(int i, int j); 回想一下指針數組的聲明:char *cars[10];同樣的把它寫成好了解的形式(非業界慣例)int* f(int i, int j);這樣一來已經十分明了了,由于( )的優先級高于*,是以f先與()結合,是以f是一個具有兩個int型參數,傳回一個指向int型指針的函數。

C語言的庫函數中有很多都是指針函數,比如字元串處理函數,下面給出一些函數原型:

char *strcat( char *dest, const char *src );

char *strcpy( char *dest, const char *src );

char *strchr( const char *s, int c );

char *strstr( const char *src, const char*sub );

注意函數的傳回值不僅僅局限于指向變量的指針,也可以是指向函數的指針。初遇這種函數的聲明可能會痛苦一點兒,但練習兩三次應該是可以了解并掌握的。首先來看這個聲明:int (*function(int)) (double*,char);要了解此聲明的含義,首先來看function(int),将function聲明為一個函數,它帶有一個int型的形式參數,這個函數的傳回值為一個指針,正是我們本将開頭講過的函數指針int (*) (double*, char);這個指針指向一個函數,此函數傳回int型并帶有兩個分别是double*型和char型的形參。如果使用typedef可以将這個聲明簡化:

typedef int (*ptf) (double*, char);

ptf function(int );

要說明一下,對于typedef int (*ptf) (double*,char); 注意不要用#define的思維來看待typedef,如果用#define的思維來看的話會以為(*ptf)(double*, char)是int的别名,但這樣的别名看起來好像又不是合法的名字,于是會處于迷茫狀态。實際上,上面的語句把ptf定義為一種函數指針類型的别名,它和函數指針類型int (*) (double*, char);等價,也就是說ptf現在也是一種類型。

3.函數指針和指針函數的混合使用

函數指針不僅可以作為傳回值類型,還可以作為函數的形式參數,如果一個函數的形參和傳回值都是函數指針,這個聲明看起來會更加複雜,例如:

void (*signal (int sig, void (*func) (intsiga)) ) ( int siga );看上去确實有些惱人,我們來一步一步的分析。現在要分析的是signal,因為緊鄰signal的是優先級最高的括号,首先與括号結合,是以signal為一個函數,括号内為signal的兩個形參,一個為int型,一個為指向函數的指針。接下來從向左看,*表示指向某對象的指針,它所處的位置表明它是signal的傳回值類型,現在可以把已經分析過的signal整體去掉,得到void (*) ( int siga ),很清晰了吧。又是一個函數指針,這個指針與signal形參表中的第二個參數類型一樣,都是指向接受一個int型形參且不傳回任何值的函數的指針。同樣地,用typedef可以将這個聲明簡化:

typedef void (*p_sig) (int);

p_sig signal(int sig, p_sig func);

這個signal函數是C語言的庫函數,在signal.h中定義,用來處理系統中産生的信号,是UNIX/Linux程式設計中經常用到的一個函數,是以在此單獨拿出來講解一下。

4.函數指針數組

還有一種較為常用的關于函數指針的用法——函數指針數組。假設現在有一個檔案處理程式,通過一個菜單按鈕來選擇相應的操作(打開檔案,讀檔案,寫檔案,關閉檔案)。這些操作都實作為函數且類型相同,分别為:

void open( );

void read( );

void write( );

void close( );

現在定義一個函數指針類型的别名PF:typedefvoid (*PF) ( );把以上4種操作取位址放入一個數組中,得到:

PF file_options[ ] = {

                &open,

                &read,

                &write,

                &close

};

這個數組中的元素都是指向不接受參數且不傳回任何值的函數的指針,是以這是一個函數指針數組。接下來,定義一個函數指針類型的指針action并初始化為函數指針數組的第一個元素:PF* action = file_options;,如果不好了解,可以類比一下int ia[4] = {0, 1, 2, 3}; int *ip = ia;,這裡PF相當于int,這樣應該比較好懂了。通過對指針action進行下标操作可以調用數組中的任一操作,如:action[2]( )會調用write操作,以此類推。在實際中,指針action可以和滑鼠或者其他GUI對象相關聯,以達到相應的目的。

5.關于指針的複雜聲明

第4點中的函數指針數組采用了typedef來聲明,這是應該提倡的方法,因為它可讀性更高。如果不使用typedef,那麼分析起來就會比較複雜,結果是void (*file_options[ ]) ( );對于C語言的複雜聲明我不想講太多,因為在實際中用到的機會并不多,并且推薦大家多用typedef來簡化聲明的複雜度。對于分析複雜聲明有一個極為有效的方法——右左法則。右左法則的大緻描述為:從未定義的變量名開始閱讀聲明,先向右看,然後向左看。當遇到括号時就調轉閱讀的方向。括号内的所有内容都分析完畢就跳出括号。這樣一直繼續下去,直到整個聲明都被分析完畢。來分析一個的例子:int * (* (*fp) (int) ) [10]; 

閱讀步驟:

1.從未定義的變量名開始閱讀 --------------------------------------------fp

2.往右看,什麼也沒有,遇到了),是以往左看,遇到一個* ------一個指向某對象的指針

3.跳出括号,遇到了(int) -----------------------------------一個帶一個int參數的函數

4.向左看,發現一個* ---------------------------------------(函數)傳回一個指向某對象的指針

5.跳出括号,向右看,遇到[10] ------------------------------一個10元素的數組

6.向左看,發現一個* ---------------------------------------一個指向某對象指針

7.向左看,發現int -----------------------------------------int類型

是以fp是指向函數的指針,該函數傳回一個指向數組的指針,此數組有10個int*型的元素。

對此我不再多舉例了,下面給出一些聲明,有興趣的朋友可以試着分析一下,答案我會在下一講中給出:

1. int *( *( *a[5]) ( ) ) ( );

2. void * (*b) ( char, int (*) ( ) );

3. float ( *(*c[10]) (int*) ) [5]; 

4. int ( *(*d)[2][3] ) [4][5];

5. int (*(*(*e) ( int* ))[15]) (int*);

6. int ( *(*f[4][5][6]) (int*) ) [10];

7. int *(*(*(*g)( ))[10]) ( );

前言

1.指針到底是什麼

2.指針的定義及運算

3.指針與數組的“愛恨情仇”

5.指針與結構

6.使用指針時的“陷阱”

後記

轉:http://blog.csdn.net/porscheyin/article/details/3461632