(三十六)函数指针与回调机制

函数指针

不只变量有地址,函数也有地址

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

每个函数在编译后都对应一串指令,这些指令在内存中的位置就是函数的地址

我们可以用一个指针类型来表示函数的地址

void (*p) (int);
//变量名为p,变量类型为函数指针,记作void (int)* ,返回值为void,参数为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进制整数。

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

与普通指针对比

//普通指针:用于读写目标内存的值
int *p;
p = &a;
*p = 123;

//函数指针:用于调用目标函数
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;
}

注意

&可以舍去,但是为了和普通变量形式上统一起来,最好还是加上

p = &example;
p = example

函数指针的使用

使用typedef可以替换掉void (*p) (int),后者可读性很差。

使用typedef给函数指针类型起个别名

#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;
}

函数指针可以作为函数的参数

#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;
}

函数指针作为成员变量

class Object
{
public:
    MY_FUNCTION m_func;
};

C语言里的回调机制

函数指针的应用场景:回调(callback)

我们调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call

如果别人的库里面调用我们的函数,就叫Callback

要拷贝一个文件,将1.pdf拷贝为1_copy.pdf

方法:调用Windows API里面有一个CopyFile函数,这种就叫调用Call

注意事先将项目的unicode字符集改为多字节字符集

#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函数就需要默默等待,用户不知道要多久,而且也不能取消

用户体验差,缺少交互性

我们希望显示拷贝的进度

比如我们提供一个函数

void CopyProgress(int total,int copied)
{
    
}

我们希望系统能时不时调用这个函数,将total/copied数据通知给我们

这就要使用函数指针,将我们函数的地址作为一个参数传给系统API即可

使用CopyFileEx(系统API的另一个函数)

  • 提供一个函数

    DWORD CALLBACK CopyProgress(...)
    
  • 将函数指针传给CopyFileEx

    CopyFileEx(source ,dst ,CopyProgress...)
    //每拷贝到一定的字节数,就会调用到我们的函数
    
#include <stdio.h>
#include <Windows.h>

// 将LARGE_INTTEGER类型转成unsigned long long
unsigned long long translate(LARGE_INTEGER num)
{
    unsigned long long result = num.HighPart;
    result <<= 32;
    result += num.LowPart;
    return result;
}

// 回调函数
// 注:要求将此函数用关键字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)
{
    // 文件的总字节数 TotalFileSize
    unsigned long long total = translate(TotalFileSize);

    // 已经完成的字节数
    unsigned long long copied =  translate(TotalBytesTransferred);

    // 打印进度
    printf("进度: %I64d / %I64d \n", copied, total); // 64位整数用 %I64d

    //printf("进度: %d / %d \n", (int)copied, (int)total); // 文件大小于2G时,可以转成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");

    // 将函数指针传给CopyFileEx
    BOOL result = CopyFileEx(source, dst, &CopyProgress, NULL, NULL, 0);

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

    return 0;
}

回调函数的上下文

回调函数总有一个参数用于传递上下文信息,上下文:Context

比如

BOOL WINAPI CopyFileEx(
    ...
    LPPROGRESS_ROUTINE lpProgressRoutine,//回调函数
    LPVOID lpData,   //上下文对象void*,只要是一个指针就行,不关心是什么类型的
    ...);

如果我们希望显示[当前用户]源文件->目标文件 :百分比

然而,上节代码CopyProgress的参数里并没有源文件名和目标文件名

也就是说只能计算百分比,无法得知当前正在拷贝的是哪个文件

观察里面有一个参数LPVOID lpData

上下文对象:携带了所有必要的上下文信息

可以定义为任意数据,由用户决定

比如

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类型转成unsigned long long
unsigned long long translate(LARGE_INTEGER num)
{
    unsigned long long result = num.HighPart;
    result <<= 32;
    result += num.LowPart;
    return result;
}

// 回调函数
// 注:要求将此函数用关键字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强制转为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");

    // 将函数指针传给CopyFileEx
    BOOL result = CopyFileEx(ctx.source, ctx.dst,
        &CopyProgress,  // 待回调的函数
        &ctx,           // 上下文对象
        NULL, 0);

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

    return 0;
}

上下文对象为void*类型,他是透传的(透明的,不关心类型与内容)

C++里的回调实现

c++里用class语法来实现回调,比如有人提供一个类库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

用户只要自己实现一个AfCopyFileListener对象,传给这个函数就行了

#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;
}

回调函数的缺点:使代码变得难以阅读,我们应该尽量避免使用回调机制,最好采用单向的函数调用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容