這裡研究的內容是函數指針,需要我們在研究後構造程序來描述函數指針數組的用法和向函數傳函數指針的方法。
指針有很多種:整型指針、結構體指針、數組指針等等,它們的本質是它們的值都是一個地址,只不過整形指針的值是一個int型數據的地址,結構體指針的值是一個結構體變量的地址,而這裡的函數指針指向的不是一個固定類型數據的地址了,而是一個函數的入口地址。
我們知道int a(char,char);是返回值為int類型,參數為char、char類型的函數a,而書上說int (*a)(char,char);是返回值為int類型、參數為char、char的函數的函數指針變量,要注意這裡a是一個函數指針,它存放的是一個地址,它的大小為2字節,而且它是要當指針用而不是當函數用,即它只能賦值、取值、取址、加減、與同類型指針比較大小,而不能傳參、返回值。那麼這裡*a為什麼要用括號括起來呢,如果不用會怎樣?如果不用括號的話就是int * a(char,char),我們知道其實int * p;也可以看成(int *)p,即指針p是一個int *型的數據,所以int *a(char,char)可以看成是(int *)a(char,char),那麼這就是一個參數為char、char型,返回值為int *型指針的函數了。它是一個函數而不是一個指針,不要以為它返回的是指針類型就是函數指針了。查閱資料可知返回指針型變量的函數叫做指針函數。指針函數可以寫成int * p(char,char)或者(int *)p(char,char),即返回值的類型可以不加括號,但是函數指針必須寫成int (* p)(char,char),也就是*p一定要加括號。
那麼函數指針的類型怎麼描述呢?整形變量int a的類型是int,函數指針int (*a)(char,char)的類型就是int (*)(char,char)。
下面我們來看一看程序1:
執行結果:
觀察程序可以發現,程序打印出了main函數和f函數的入口地址,將f的地址賦給了三個不同類型的變量並將它們打印出來,然後以兩種不同的方式調用函數f並將返回值打印出來。觀察程序可以發現以下問題:
(1)p=f在這裡是將函數的入口地址賦給指針p,這說明函數的名字就代表它的入口地址,這不像我們對一些變量的地址進行操作要用到取址符&,而變量名則代表變量的值,比如int a,a表示a存儲的值而&a才表示它的地址,但是數組的使用和上式很相似,數組名代表的是數組的首地址而不是第一個元素的值。如下面程序所驗證的:
(2)由a=p(1,2);語句,我們發現如果把函數f的入口地址賦給指針p,那麼可以用指針p來代替f調用f函數,這是為什麼呢?我們之前的研究指出函數名存儲的是函數的入口地址,從匯編角度看,要調用函數,先將函數的參數入棧,再跳轉到函數名所表示的地址並運行函數裡的語句。這裡的函數名起的作用就是表示函數的地址,讓函數被調用的時候能夠跳轉到函數的地址,那麼我們將函數地址賦給函數指針之後,函數指針的值就是函數的入口地址,那麼函數指針完全可以代替函數名來表示函數的地址。那麼既然可以用函數名表示函數的地址,那麼為什麼要再用函數指針呢,函數指針有什麼意義呢?這個問題我們後面再研究。
因為我們之前將函數名的值強制轉換成int型賦給了變量b,所以我們也可以用b來表示函數的地址,只不過要先將它再轉換成函數指針類型,這樣才能表示一個地址。
(3)我們發現函數f的返回值是int型,而它的返回值等於a+b,但是a和b都是char型變量,為什麼兩個char型變量相加可以返回一個int型變量呢?查閱資料,發現函數在返回時如果要返回的值的類型與函數的類型不同,那麼會使用強制類型轉換將要返回的值的類型強制轉換成函數的類型再返回,即使函數的類型比要返回的值的類型小也是這樣,如下圖:
再看程序2:
這個程序輸出了main函數的偏移地址還有它的段地址加偏移地址、f函數的偏移地址還有它的段地址加偏移地址、還有p、b、c、a的值。與上一個程序不同的是這裡的函數指針p被定義成遠指針,這裡far要寫在括號裡*p的前面,這樣將f賦給p則p存儲的是f的段地址加偏移地址。這裡我們可以將c強制轉換為函數指針的類型再代替f調用函數,但不能將b強制轉換再使用,因為這裡b是一個int型的變量,他只存儲了偏移地址的數據而沒有段地址的數據,如果轉換成far指針會出錯。
那麼再回到開始的問題:怎麼構造程序來描述函數指針數組的用法和向函數傳函數指針的方法呢?函數指針數組首先是一個數組,數組裡的元素是函數指針,也就是每一個元素都是一個地址,指向一個函數入口。所以數組有幾個元素,就要有幾個函數。而向函數傳函數指針,就是把函數指針作為函數的參數,需要在定義和聲明時將函數的參數定義為函數指針。程序如下:
這裡定義了一個函數指針數組a,並將函數f1、f2、f3的地址分別存放到數組中,之後調用函數f,並用數組將函數f1、f2、f3的地址作為參數提供給f。在函數f裡根據傳進來的參數對函數f1、f2、f3進行了調用,將f1、f2、f3的返回值相加並返回到main函數,main函數再將f的返回值打印出來。這裡我們定義並利用了函數指針數組,向函數裡傳遞了函數指針。
1、既然可以用函數名表示函數的地址,那麼為什麼要再用函數指針呢,函數指針有什麼意義呢?
答:可以說一個函數名就相當於一個函數指針,但是只是這一個函數的函數指針,我們使用函數指針,可以隨時改變它的值,讓它指向不同的函數以方便使用。比如高級語言實現一個下拉菜單,其實就是每個菜單項是一個函數,定義一個函數指針,當用戶選擇一個選項時,讓函數指針指向它對應的函數執行,即實現了相應功能。
2、我們先來看一個題目:有一段程序存儲在起始地址為 0的一段內存上,如果我們想要調用這段程序,請問該如何去做?答案是 (*(void (*)( ) )0)( )。很顯然我們調用的這個函數是沒有參數的,而0是它的地址,所以我們可以把0轉換成函數指針,讓它指向這個函數,即(void(*))0。但是這只相當於地址0000:0000,那麼它就相當於函數名啊,函數不就是(void(*))0()了,為什麼答案是 (*(void (*)( ) )0)( )呢?還有int *p表示從以p的值為地址所指向的那個空間裡取出大小為int類型的值。那麼定義(void(*p))()的話,*p表示取的值的大小是多少?結果查閱資料發現void型指針不能復引用,即*p是錯誤的用法。
3、函數指針在定義的時候一定要定義函數的參數類型和個數嗎?
答:可以不定義。
我們之前學習指針主要是學習數據類型的指針,這類指針的特點是存儲一地址,這個地址存放的是指定的空間,而函數指針指向的是一個函數。但是這裡函數指針裡的數據類型(如int(*p)(char))表示的是函數的返回值類型,那麼程序怎麼知道函數有多長,該在內存裡取多少數據呢?我覺得是通過函數的返回語句判斷函數結束了。
我覺得函數指針容易弄錯的地方就是它後面跟了函數的參數類型,導致我們在傳參時老想將這些參數傳進去,而實際上要傳的是一個指針。還有從別的指針的定義可以直接看出指向的空間大小,而函數指針只能看出函數的返回類型。
從宏觀來看,函數指針讓我們調用函數時也能夠直接從內存地址調用了,這充分說明了c語言的自由性,我們可以用它寫出十分精簡的程序,但是這樣也容易造成使用出錯,我們在使用時要小心。