美文网首页C/C++
(三十六)函数指针与回调机制

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

作者: RGiskard | 来源:发表于2018-11-11 16:23 被阅读1次

    函数指针

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

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

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

    相关文章

      网友评论

        本文标题:(三十六)函数指针与回调机制

        本文链接:https://www.haomeiwen.com/subject/pwoxfqtx.html