(三十六)函数指针与回调机制
函数指针
不只变量有地址,函数也有地址
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;
}
回调函数的缺点:使代码变得难以阅读,我们应该尽量避免使用回调机制,最好采用单向的函数调用。