搞懂C語言函數指針

来源:https://www.cnblogs.com/bianchengzhuji/archive/2019/01/04/10222335.html
-Advertisement-
Play Games

前言 函數指針是什麼?如何使用函數指針?函數指針到底有什麼大用?本文將一一介紹。 如何理解函數指針 如果有int *類型變數,它存儲的是int類型變數的地址;那麼對於函數指針來說,它存儲的就是函數的地址。函數也是有地址的,函數實際上由載入記憶體的一些指令組成,而指向函數的指針存儲了函數指令的起始地址。 ...



 

前言

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

如何理解函數指針

如果有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,資料庫等編程相關[原創]技術文章,號內包含大量經典電子書和視頻學習資源。歡迎一起交流學習,一起修煉電腦“內功”,知其然,更知其所以然。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 介面體現的是一種規範和實現分離的設計哲學,充分利用介面可以極大的降低程式中各個模塊之間的耦合,提高系統的可維護性以及可擴展性。 ...
  • 對於大部分的對象而言,程式里會有一個引用變數來引用該對象,這是最常見的引用方法。除此之外,java.lang.ref包下還提供了3個類:SoftReference、WeakReference和PhantomReference。它們分別代表了系統對對象的另外3中引用方式:軟引用、弱引用和虛引用。 ...
  • 文/沉默王二 開門見山地說吧,Java提供了一套完整的集合類(也可以叫做容器類)來管理一組長度可變的對象(也就是集合的元素),其中常見的類型包括List、Set、Queue和Map。從我個人的編程經驗來看,List的實現類ArrayList和Map的實現類HashMap使用頻率最高,其它實現類只能望 ...
  • /*懶漢模式 *優點:延遲載入 * 缺點:不加同步的懶漢模式是線程不安全的,加了synchronzide之後就變成線程安全的了 */public class Singleton { private static Singleton singleton=null; private Singleton( ...
  • Django 系列博客(二) 前言 今天博客的內容為使用 Django 完成第一個 Django 頁面,併進行一些簡單頁面的搭建和轉跳。 命令行搭建 Django 項目 創建純凈虛擬環境 在上一篇博客中已經安裝好了虛擬環境,所以用虛擬環境來安裝指定版本的 Django。為了可以從頭到尾的走一遍流程, ...
  • 計數器 Counter 計數元素迭代器 elements() 計數對象拷貝 copy() 計數對象清空 clear() 有序字典 OrderedDict (對字典的補充,可以記住字典元素添加的順序) 預設字典 defaultdict,(指定字典值的類型) 可命名元組 namedtuple (給元組對 ...
  • 網路編程協議 1.osi七層模型 應用層 表示層 會話層 傳輸層 網路層 數據鏈路層 物理層 2.套接字 socket 有兩類,一種基於文件類型,一種基於網路類型 3.Tcp和udp協議 Tcp協議:面向連接,數據可靠,傳輸效率低,面向位元組流 建立連接與斷開連接的過程(三次握手,四次揮手) 建立連接 ...
  • 1.學習爬蟲,為什麼必須會正則表達式? 我們爬取一些網頁具體內容時,只需要這個網頁某個標簽的一部分內容就足夠,或者是這個標簽的某個屬性的值時,用普通的 xpath 或者css.selector是不能完成的,此時我們就需用到正則表達式去匹配獲取。2.正則表達式官方簡介? 正則表達式,又稱規則表達式。( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...