(三十六)函數(shù)指針與回調(diào)機制

函數(shù)指針

不只變量有地址犁钟,函數(shù)也有地址

void example(int n)
{
    printf("%d\n",n);
}
int main()
{
    //打印函數(shù)的地址
    printf("%08X\n",&example);
    //printf("%p\n",&example);
    return 0;
}

每個函數(shù)在編譯后都對應一串指令,這些指令在內(nèi)存中的位置就是函數(shù)的地址

我們可以用一個指針類型來表示函數(shù)的地址

void (*p) (int);
//變量名為p,變量類型為函數(shù)指針从隆,記作void (int)* 歉眷,返回值為void,參數(shù)為int
void example(int n)
{
    printf("%d\n",n);
}
int main()
{
    void (*p) (int);
    p = &example;
    return 0;
}
void example(int a,int b)
{
    printf("%d,%d\n",a,b);
}
int main()
{
    void (*p) (int,int);
    p = &example;
    return 0;
}

第一個也可以寫作

//可讀性較差
void (*p) (int) = &example;

指針變量也是變量材诽,其實所有的指針都是整型底挫,08X打印出來都是8位16進制整數(shù)。

void ex1(int n)
{
    printf(...);
}
void ex2(int n)
{
    printf(...);
}
int main()
{
    void (*p) (int);
    //先指向ex1脸侥,再指向ex2
    p = &ex1;
    p = &ex2;
    return 0;
}

與普通指針對比

//普通指針:用于讀寫目標內(nèi)存的值
int *p;
p = &a;
*p = 123;

//函數(shù)指針:用于調(diào)用目標函數(shù)
void (*p) (int);
p = &example;
p(123);
#include<stdio.h>
void example(int n)
{
    printf("%d\n",n);
}
int main()
{
    void (*p) (int) = &example;
    p(1);
    return 0;
}

注意

&可以舍去建邓,但是為了和普通變量形式上統(tǒng)一起來,最好還是加上

p = &example;
p = example

函數(shù)指針的使用

使用typedef可以替換掉void (*p) (int)睁枕,后者可讀性很差官边。

使用typedef給函數(shù)指針類型起個別名

#include<stdio.h>
void example(int n)
{
    printf("%d\n",n);
}
typedef void (*MY_FUNCTION) (int);

int main()
{
    MY_FUNCTION p;
    p = &example;
    p(1);
    return 0;
}

函數(shù)指針可以作為函數(shù)的參數(shù)

#include<stdio.h>
void example(int n)
{
    printf("%d\n",n);
}
typedef void (*MY_FUNCTION) (int);

void test(MY_FUNCTION f)
{
    f(123);
}
int main()
{
    test(&example);
    
    //MY_FUNCTION p;
    //p = &example;
    //test(p);

    return 0;
}

函數(shù)指針作為成員變量

class Object
{
public:
    MY_FUNCTION m_func;
};

C語言里的回調(diào)機制

函數(shù)指針的應用場景:回調(diào)(callback)

我們調(diào)用別人提供的 API函數(shù)(Application Programming Interface,應用程序編程接口),稱為Call

如果別人的庫里面調(diào)用我們的函數(shù)外遇,就叫Callback

要拷貝一個文件注簿,將1.pdf拷貝為1_copy.pdf

方法:調(diào)用Windows API里面有一個CopyFile函數(shù),這種就叫調(diào)用Call

注意事先將項目的unicode字符集改為多字節(jié)字符集

#include<stdio.h>
#include<Windows.h>

int main()
{
    const char* source = "D:\\Document\\1.pdf";
    const char* dst    = "D:\\Document\\1_copy.pdf";
    BOOL result = CopyFile(source,dst,FALSE);
    printf("操作完成:%s\n",result ? "success": "failed");
    return 0;
}

何時需要Callback跳仿?

若拷貝一個很大的文件诡渴,這個拷貝過程需要很多時間,如果用CopyFile函數(shù)就需要默默等待菲语,用戶不知道要多久妄辩,而且也不能取消

用戶體驗差,缺少交互性

我們希望顯示拷貝的進度

比如我們提供一個函數(shù)

void CopyProgress(int total,int copied)
{
    
}

我們希望系統(tǒng)能時不時調(diào)用這個函數(shù)谨究,將total/copied數(shù)據(jù)通知給我們

這就要使用函數(shù)指針恩袱,將我們函數(shù)的地址作為一個參數(shù)傳給系統(tǒng)API即可

使用CopyFileEx(系統(tǒng)API的另一個函數(shù))

  • 提供一個函數(shù)

    DWORD CALLBACK CopyProgress(...)
    
  • 將函數(shù)指針傳給CopyFileEx

    CopyFileEx(source ,dst ,CopyProgress...)
    //每拷貝到一定的字節(jié)數(shù),就會調(diào)用到我們的函數(shù)
    
#include <stdio.h>
#include <Windows.h>

// 將LARGE_INTTEGER類型轉(zhuǎn)成unsigned long long
unsigned long long translate(LARGE_INTEGER num)
{
    unsigned long long result = num.HighPart;
    result <<= 32;
    result += num.LowPart;
    return result;
}

// 回調(diào)函數(shù)
// 注:要求將此函數(shù)用關鍵字CALLBACK修飾(這是Windows API的要求)
DWORD CALLBACK CopyProgress(  
    LARGE_INTEGER TotalFileSize,
    LARGE_INTEGER TotalBytesTransferred,
    LARGE_INTEGER StreamSize,
    LARGE_INTEGER StreamBytesTransferred,
    DWORD dwStreamNumber,
    DWORD dwCallbackReason,
    HANDLE hSourceFile,
    HANDLE hDestinationFile,
    LPVOID lpData)
{
    // 文件的總字節(jié)數(shù) TotalFileSize
    unsigned long long total = translate(TotalFileSize);

    // 已經(jīng)完成的字節(jié)數(shù)
    unsigned long long copied =  translate(TotalBytesTransferred);

    // 打印進度
    printf("進度: %I64d / %I64d \n", copied, total); // 64位整數(shù)用 %I64d

    //printf("進度: %d / %d \n", (int)copied, (int)total); // 文件大小于2G時胶哲,可以轉(zhuǎn)成int

    return PROGRESS_CONTINUE;
}

int main()
{
    const char* source = "D:\\Download\\1.Flv";
    const char* dst    = "D:\\Download\\1_copy.Flv";

    printf("start copy ...\n");

    // 將函數(shù)指針傳給CopyFileEx
    BOOL result = CopyFileEx(source, dst, &CopyProgress, NULL, NULL, 0);

    printf("operation done : %s \n", result ? "success" : "failed");

    return 0;
}

回調(diào)函數(shù)的上下文

回調(diào)函數(shù)總有一個參數(shù)用于傳遞上下文信息畔塔,上下文:Context

比如

BOOL WINAPI CopyFileEx(
    ...
    LPPROGRESS_ROUTINE lpProgressRoutine,//回調(diào)函數(shù)
    LPVOID lpData,   //上下文對象void*,只要是一個指針就行,不關心是什么類型的
    ...);

如果我們希望顯示[當前用戶]源文件->目標文件 :百分比

然而澈吨,上節(jié)代碼CopyProgress的參數(shù)里并沒有源文件名和目標文件名

也就是說只能計算百分比把敢,無法得知當前正在拷貝的是哪個文件

觀察里面有一個參數(shù)LPVOID lpData

上下文對象:攜帶了所有必要的上下文信息

可以定義為任意數(shù)據(jù),由用戶決定

比如

struct Context
{
    char username[32],
    char source[128],
    char dst[128]
};

這樣就能顯示我們想要的了

#include <stdio.h>
#include <Windows.h>

// 文件拷貝所需的上下文信息
struct Context
{
    char username[32];
    char source[128];
    char dst[128];
};

// 將LARGE_INTTEGER類型轉(zhuǎn)成unsigned long long
unsigned long long translate(LARGE_INTEGER num)
{
    unsigned long long result = num.HighPart;
    result <<= 32;
    result += num.LowPart;
    return result;
}

// 回調(diào)函數(shù)
// 注:要求將此函數(shù)用關鍵字CALLBACK修飾(這是Windows API的要求)
DWORD CALLBACK CopyProgress(  
    LARGE_INTEGER TotalFileSize,
    LARGE_INTEGER TotalBytesTransferred,
    LARGE_INTEGER StreamSize,
    LARGE_INTEGER StreamBytesTransferred,
    DWORD dwStreamNumber,
    DWORD dwCallbackReason,
    HANDLE hSourceFile,
    HANDLE hDestinationFile,
    LPVOID lpData) // <- 這個就是上下文件對象
{
    // 計算百分比
    unsigned long long total = translate(TotalFileSize);
    unsigned long long copied =  translate(TotalBytesTransferred);
    int percent = (int) ( (copied * 100 / total) );

    // 打印進度谅辣,將指針lpData強制轉(zhuǎn)為Context*類型
    Context* ctx = (Context*) lpData;
    printf("[用戶: %s], %s -> %s : 進度 %d %%\n", 
        ctx->username, ctx->source, ctx->dst, percent);

    return PROGRESS_CONTINUE;
}

int main()
{
    Context ctx; // 上下文對象
    strcpy(ctx.username, "dada");
    strcpy(ctx.source, "D:\\Download\\1.Flv" );
    strcpy(ctx.dst, "D:\\Download\\1_copy.Flv");

    printf("start copy ...\n");

    // 將函數(shù)指針傳給CopyFileEx
    BOOL result = CopyFileEx(ctx.source, ctx.dst,
        &CopyProgress,  // 待回調(diào)的函數(shù)
        &ctx,           // 上下文對象
        NULL, 0);

    printf("operation done : %s \n", result ? "success" : "failed");

    return 0;
}

上下文對象為void*類型帅霜,他是透傳的(透明的饵逐,不關心類型與內(nèi)容)

C++里的回調(diào)實現(xiàn)

c++里用class語法來實現(xiàn)回調(diào),比如有人提供一個類庫AfCopyFile,能提供文件拷貝功能猪叙,而且能通知用戶當前進度

int DoCopy(const char* source, const char* dst,AfCopyFile* listener);
///別人提供的AfCopyFile.h
#ifndef _AF_COPY_FILE_H
#define _AF_COPY_FILE_H

class AfCopyFileListener
{
public:
    virtual int OnCopyProgress(long long total, long long transfered) = 0;
};

class AfCopyFile
{
public:
    int DoCopy(const char* source, 
        const char* dst, 
        AfCopyFileListener* listener);
};

#endif

用戶只要自己實現(xiàn)一個AfCopyFileListener對象序六,傳給這個函數(shù)就行了

#include <stdio.h>
#include <string.h>
#include "AfCopyFile.h"

class MainJob : public AfCopyFileListener
{
public:
//  int DoJob()
//  {
//      strcpy(user, "shaofa");
//      strcpy(source, "c:\\test\\2.rmvb" );
//      strcpy(dst, "c:\\test\\2_copy.rmvb");
// 
//      AfCopyFile af;
//      af.DoCopy(source, dst, this); // 將this傳過去
//  
//      return 0;
//  }

    int OnCopyProgress(long long total, long long transfered)
    {
        // 打印進度
        int percent = (int) ( (transfered * 100 / total) );     
        printf("[用戶: %s], %s -> %s : 進度 %d %%\n", 
            user, source, dst, percent);

        return 0;
    }

public:
    char source[256];
    char dst[256];
    char user[64];
};

int main()
{
    MainJob job;
    strcpy(job.user, "shaofa");
    strcpy(job.source, "c:\\test\\2.rmvb" );
    strcpy(job.dst, "c:\\test\\2_copy.rmvb");

    AfCopyFile af;
    af.DoCopy(job.source, job.dst, &job); // 將this傳過去
    
//  job.DoJob();

    return 0;
}

回調(diào)函數(shù)的缺點:使代碼變得難以閱讀逗旁,我們應該盡量避免使用回調(diào)機制立宜,最好采用單向的函數(shù)調(diào)用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末萎河,一起剝皮案震驚了整個濱河市荔泳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌虐杯,老刑警劉巖玛歌,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異擎椰,居然都是意外死亡支子,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門达舒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來译荞,“玉大人,你說我怎么就攤上這事休弃。” “怎么了圈膏?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵塔猾,是天一觀的道長。 經(jīng)常有香客問我稽坤,道長丈甸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任尿褪,我火速辦了婚禮睦擂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杖玲。我一直安慰自己顿仇,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著臼闻,像睡著了一般鸿吆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上述呐,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天惩淳,我揣著相機與錄音,去河邊找鬼乓搬。 笑死思犁,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的进肯。 我是一名探鬼主播激蹲,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坷澡!你這毒婦竟也來了托呕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤频敛,失蹤者是張志新(化名)和其女友劉穎项郊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斟赚,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡着降,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拗军。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片任洞。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖发侵,靈堂內(nèi)的尸體忽然破棺而出交掏,到底是詐尸還是另有隱情,我是刑警寧澤刃鳄,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布盅弛,位于F島的核電站,受9級特大地震影響叔锐,放射性物質(zhì)發(fā)生泄漏挪鹏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一愉烙、第九天 我趴在偏房一處隱蔽的房頂上張望讨盒。 院中可真熱鬧,春花似錦步责、人聲如沸返顺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽创南。三九已至伦忠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間稿辙,已是汗流浹背昆码。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留邻储,地道東北人赋咽。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像吨娜,于是被迫代替她去往敵國和親脓匿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容