天天看點

搞懂C語言函數指針 - 守望先生

搞懂C語言函數指針

函數指針是什麼?難嗎?未必。

 原文位址:https://www.yanbinghu.com/2019/01/03/3593.html

前言

函數指針是什麼?如何使用函數指針?函數指針到底有什麼大用?本文将一一介紹。

如何了解函數指針

如果有int *類型變量,它存儲的是int類型變量的位址;那麼對于函數指針來說,它存儲的就是函數的位址。函數也是有位址的,函數實際上由載入記憶體的一些指令組成,而指向函數的指針存儲了函數指令的起始位址。如此看來,函數指針并沒有什麼特别的。我們可以檢視程式中函數的位址:

#include <stdio.h>
int test()
{
    printf("this is test function");
    return 0;
}
int main(void)
{
    test();
    return 0;
}
           

編譯:

gcc -o testFun testFun.c
           

檢視test函數相對位址(并非實際運作時的位址):

$ nm testFun |grep test  #檢視test函數的符号表資訊
0000000000400526 T test
           

如何聲明函數指針

聲明普通類型指針時,需要指明指針所指向的資料類型,而聲明函數指針時,也就要指明指針所指向的函數類型,即需要指明函數的傳回類型和形參類型。例如對于下面的函數原型:

int sum(int,int);
           

它是一個傳回值為int類型,參數是兩個int類型的函數,那麼如何聲明該類型函數的指針呢?很簡單,将函數名替換成(*pf)形式即可,即我們把sum替換成(*fp)即可,fp為函數指針名,結果如下:

int (*fp)(int,int);
           

這樣就聲明了和sum函數類型相同的函數指針fp。這裡說明兩點,第一,*和fp為一體,說明了fp為指針類型,第二,*fp需要用括号括起來,否則就會變成下面的情況:

int *fp(int,int);
           

這種情況下,意思就大相徑庭了,它聲明了一個參數為兩個int類型,傳回值為int類型的指針的函數,而不再是一個函數指針了。

在經常使用函數指針之後,我們很快就會發現,每次聲明函數指針都要帶上長長的形參和傳回值,非常不便。這個時候,我們應該想到使用typedef,即為某類型的函數指針起一個别名,使用起來就友善許多了。例如,對于前面提到的函數可以使用下面的方式聲明:

typedef int (*myFun)(int,int);//為該函數指針類型起一個新的名字
myFun f1;       //聲明myFun類型的函數指針f1
           

上面的myFun就是一個函數指針類型,在其他地方就可以很友善地用來聲明變量了。typedef的使用不在本文的讨論範圍,但是特别強調一句,typedef中聲明的類型在變量名的位置出現,了解了這一句,也就很容易使用typedef了。因而下面的方式是錯誤的:

typedef myFun (int)(int,int);   //錯誤
typedef (int)(int,int)  *myFun;   //錯誤
           

為函數指針指派

指派也很簡單,既然是指針,将對應指針類型賦給它既可。例如:

#include<stdio.h>
int test(int a,int b)
{
    /*do something*/
    return 0
}
typedef int(*fp)(int,int);
int main(void)
{
    fp f1 = test; //表達式1
    fp f2 = &test;//表達式2
    printf("%p\n",f1);
    printf("%p\n",f2);
    return 0;
}
           

在這裡,聲明了傳回類型為int,接受兩個int類型參數的函數指針f1和f2,分别給它們進行了指派。表達式1和表達式2在作用上并沒有什麼差別。因為函數名在被使用時總是由編譯器把它轉換為函數指針,而前面加上&不過顯式的說明了這一點罷了。

調用

調用也很容易,把它看成一個普通的函數名即可:

#include<stdio.h>
int test(int a,int b)
{
    /*do something*/
    printf("%d,%d\n",a,b);
    return 0
}
typedef int(*fp)(int,int);
int main(void)
{
    fp f = test; 
    f(1,2);//表達式1
    (*f)(3,4);//表達式2
    return 0;
}
           

在函數指針後面加括号,并傳入參數即可調用,其中表達式1和表達式2似乎都可以成功調用,但是哪個是正确的呢?ANSI C認為這兩種形式等價。

函數指針有何用

函數指針的應用場景比較多,以庫函數qsort排序函數為例,它的原型如下:

void qsort(void *base,size_t nmemb,size_t size , int(*compar)(const void *,const void *));
           

看起來很複雜對不對?拆開來看如下:

void qsort(void *base, size_t nmemb, size_t size, );
           

拿掉第四個參數後,很容易了解,它是一個無傳回值的函數,接受4個參數,第一個是void*類型,代表原始數組,第二個是size_t類型,代表資料數量,第三個是size_t類型,代表單個資料占用空間大小,而第四個參數是函數指針。這第四個參數,即函數指針指向的是什麼類型呢?

int(*compar)(const void *,const void *)
           

很顯然,這是一個接受兩個const void*類型入參,傳回值為int的函數指針。

到這裡也就很清楚了。這個參數告訴qsort,應該使用哪個函數來比較元素,即隻要我們告訴qsort比較大小的規則,它就可以幫我們對任意資料類型的數組進行排序。

在這裡函數指針作為了參數,而他同樣可以作為傳回值,建立數組,作為結構體成員變量等等,它們的具體應用我們在後面的文章中會介紹,本文不作展開。本文隻介紹一個簡單執行個體。

執行個體介紹

我們通過一個執行個體來看函數指針怎麼使用。假設有一學生資訊,需要按照學生成績進行排序,該如何處理呢?

#include <stdio.h>
#include <stdlib.h>
#define STU_NAME_LEN 16
/*學生資訊*/
typedef struct student_tag
{
    char name[STU_NAME_LEN];  //學生姓名
    unsigned int id;          //學生學号
    int score;                //學生成績
}student_t;
int studentCompare(const void *stu1,const void *stu2)
{
  /*強轉成需要比較的資料結構*/
    student_t *value1 = (student_t*)stu1;
    student_t *value2 = (student_t*)stu2;
    return value1->score-value2->score;
}
int main(void)
{
    /*建立三個學生資訊*/
    student_t stu1 = {"one",1,99};
    student_t stu2 = {"two",2,77};
    student_t stu3 = {"three",3,88};

    student_t stu[] = {stu1,stu2,stu3};

    /*排序,将studentCompare作為參數傳入qsort*/
    qsort((void*)stu,3,sizeof(student_t),studentCompare);
    int loop = 0;

    /**周遊輸出*/
    for(loop = 0; loop < 3;loop++)
    {
        printf("name:%s,id:%u,score:%d\n",stu[loop].name,stu[loop].id,stu[loop].score);
    }
    return 0;
}
           

我們建立了一個學生資訊結構,結構成員包括名字,學号和成績。main函數中建立了一個包含三個學生資訊的數組,并使用qsort函數對數組按照學生成績進行排序。qsort函數第四個參數是函數指針,是以我們需要傳入一個函數指針,并且這個函數指針的入參是cont void *類型,傳回值為int。我們通過前面的學習知道了函數名本身就是指針,是以隻需要将我們自己實作的studentCompare作為參數傳入即可。

最終運作結果如下:

name:two,id:2,score:77
name:three,id:3,score:88
name:one,id:1,score:99
           

可以看到,最終學生資訊按照分數從低到高輸出。

總結

本文介紹了函數指針的聲明和簡單使用。更多使用将在後面的文章介紹,本文總結如下:

  • 函數指針與其他指針類型無本質差異,不過它指向的是函數的位址罷了。
  • 聲明函數指針需要指明函數的傳回類型和形參類型。
  • 函數名在被使用時總是由編譯器把它轉換為函數指針。
  • 要想聲明函數指針,隻需寫出函數原型,然後将函數名用(*fp)代替即可。這裡fp是聲明的函數指針變量。
  • typedef中聲明的類型在變量名的位置出現。
微信公衆号【程式設計珠玑】:專注但不限于分享計算機程式設計基礎,Linux,C語言,C++,Python,資料庫等程式設計相關[原創]技術文章,号内包含大量經典電子書和視訊學習資源。歡迎一起交流學習,一起修煉計算機“内功”,知其然,更知其是以然。