Giskard

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

2018-11-01

函数指针

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

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

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

Tags: C/C++