回调的设想
回调函数
main.cpp
#include <iostream>
#include <functional>
#include <fstream>
#include "mclog.h"
// 写入回调
struct write_file
{
// 普通函数
write_file(const std::string &file, void (*fn_write)(std::ofstream &))
{
std::ofstream fs(file);
if (fs.is_open())
{
fn_write(fs);
fs.close();
}
}
};
// 读取回调
struct read_file
{
// function 类型
read_file(const std::string &file, std::function<void(const std::string &content)> fn_read = nullptr)
{
std::string ret;
std::ifstream fs(file);
if (fs.is_open())
{
std::string buff;
while (std::getline(fs, buff))
{
ret += buff + "\n";
}
if (fn_read)
{
fn_read(ret);
}
fs.close();
}
}
};
// 回调的处理函数
void fun_write(std::ofstream &fs)
{
fs << "食材:猪肉,牛肉,辣椒,白菜,豆腐" << std::endl;
fs << "配料:酱油,沙姜,葱花,大蒜" << std::endl;
fs << "主食:大米,面条,馒头" << std::endl;
MCLOG("写入函数触发");
}
// 回调的处理函数
void fun_read(const std::string &content)
{
MCLOG("普通函数读取\n" $(content))
}
int main(int argc, char **argv)
{
std::string filename = "food_list.txt";
// 回调写入-使用普通函数处理
write_file(filename, fun_write);
// 回调读取-使用普通函数处理
read_file(filename, fun_read);
// 回调读取-使用匿名函数处理(lambda)
read_file(filename, [=](const std::string &content) {
MCLOG("匿名函数读取\n" $(content))
MCLOG("文件名为: " $(filename));
});
// lambda 生命周期
MCLOG("生命周期");
std::function<void()> fn_copy = nullptr;
std::function<void()> fn_ref = nullptr;
{
int value = 100;
int *pvalue = &value;
fn_copy = [=]()
{
MCLOG($(value));
};
fn_ref = [&]()
{
MCLOG($(value));
};
MCLOG("销毁前执行");
fn_copy();
fn_ref();
// 模拟为销毁后的数据改变
// value 被销毁,被销毁的值是未知的,未定义行为
*pvalue = 200;
}
MCLOG("销毁后执行");
fn_copy();
fn_ref();
return 0;
}
打印结果
写入函数触发 [/home/red/open/github/mcpp/example/15/main.cpp:52]
普通函数读取
[content: 食材:猪肉,牛肉,辣椒,白菜,豆腐
配料:酱油,沙姜,葱花,大蒜
主食:大米,面条,馒头
] [/home/red/open/github/mcpp/example/15/main.cpp:58]
匿名函数读取
[content: 食材:猪肉,牛肉,辣椒,白菜,豆腐
配料:酱油,沙姜,葱花,大蒜
主食:大米,面条,馒头
] [/home/red/open/github/mcpp/example/15/main.cpp:74]
文件名为: [filename: food_list.txt] [/home/red/open/github/mcpp/example/15/main.cpp:75]
生命周期 [/home/red/open/github/mcpp/example/15/main.cpp:83]
销毁前执行 [/home/red/open/github/mcpp/example/15/main.cpp:94]
[value: 100] [/home/red/open/github/mcpp/example/15/main.cpp:88]
[value: 100] [/home/red/open/github/mcpp/example/15/main.cpp:91]
销毁后执行 [/home/red/open/github/mcpp/example/15/main.cpp:102]
[value: 100] [/home/red/open/github/mcpp/example/15/main.cpp:88]
[value: 200] [/home/red/open/github/mcpp/example/15/main.cpp:91]
回调函数,一种对未来可能的预设,你会预设别人会用什么,然后预设你所提供的内容是别人想要的,这只是回调的简单用法之一
回调函数通常是为了将不同的功能分开编写,然后在你的函数里面使用回调函数去处理别人的功能
回调函数通常跟异步逻辑有关,但现在还为涉及到异步相关内容,这部分在后续文章内补充
回调函数
在 main.cpp 文件中,我先写入一段内容到文件,但是我没有直接通过 ofstream 对象写入,而是创建了 write_file 类,在类的构造函数中存在 fn_write 变量,通过这个变量写入内容,然后就关闭文件
这个 fn_write 变量是函数指针,这个变量存储的是函数地址,函数指针可以像普通函数一样去执行,也可以像普遍变量一样当作参数传递,fn_write 函数指针被当成变量来使用,这个被调用的函数就被称为回调函数
在 write_file 构造函数中,请注意 fn_write 函数指针可接收的函数类型,传入的函数需要与 fn_write 的函数类型保持一致才行,在调用时传入的是 fun_write 函数和 fn_write 的函数声明是一样的,声明包括返回值,参数个数,参数类型
当传入 fun_write 函数之后,fn_write 函数指针就指向 fun_write 函数,等于绑定了关系,执行 fn_write 就等于执行 fun_write ,这一点应该不难明白
函数的可替换性
回调函数的意义在于可以被随时替换掉,正执行的是函数指针,而不是具体的函数,原因就在于函数指针是可以改变指向的
在 read_file 类中,我读取了两遍文件,但是他们的打印是不一样的,因为他们指向的不是同一个函数,这就是通过传入不一样的函数实现不一样的功能,其实这一点和面向对象的多态效果是一致的,你可以在调用时动态的决定代码的走向
回调的细节
通过 main.cpp 代码你可能已经发现,我使用了保护普通函数,匿名函数(lambda),函数指针,std::function 类对象这几种方式混合编写了回调函数,其实我只想告诉你可以这么干
实际上我只推荐你使用 std::function 和 lambda 编写回调,使用 lambda 的意义在于每一次回调都是可能不一样的,而且每一次你都可以随时调整你的回调执行任务
相比与传统的函数指针,std::function 在使用上更像一种变量,它可以很明确的声明一种函数类型,然后在后续中调用,我推荐你使用 std::function ,它可以代替函数指针,但是你要注意的是, std::function 是类对象,所以建议在执行时判空,否则传入为空时执行会直接崩溃
回调的复用性
其实编写回调主要有两个用途,一个是网络的异步编程,一个是功能复用
你可以从 read_file 两个类中看出,打开文件需要执行文件三部曲,然后在读取全部内容到字符串,使用回调可以在成功读取内容之后执行回调任务,如果打开失败我就不处理了,如果是使用普通函数处理方式,我需要返回打开成功或者失败,在外部判断是否成功或者失败才能对字符串进行处理
那如果我需要处理失败,使用回调的方式就是在传入一个错误回调函数即可
lambda传参
std::function<void()> fn_copy = nullptr;
std::function<void()> fn_ref = nullptr;
{
int value = 100;
int *pvalue = &value;
fn_copy = [=]()
{
MCLOG($(value));
};
fn_ref = [&]()
{
MCLOG($(value));
};
MCLOG("销毁前执行");
fn_copy();
fn_ref();
// 模拟为销毁后的数据改变
// value 被销毁,被销毁的值是未知的,未定义行为
*pvalue = 200;
}
MCLOG("销毁后执行");
fn_copy();
fn_ref();
如果你已经学习过 lambda 就会发现,匿名函数可以获取到当前作用域的变量数据,但是当你使用这些变量时,这个匿名函数总是莫名其妙的崩溃
这可能是你没有了解参数的声明周期导致的,使用 [=] 是拷贝行为, [&] 是引用行为,拷贝就是自己存在一份副本,而引用则使用原来的数据
当使用引用时,一旦引用的对象声明周期结束,你的程序获取的数据就是未定义的,可能是原来的值,也可能是乱码,所以就会引发崩溃
可以看到 fn_copy fn_ref 在变量销毁后,fn_copy 可以打印原来的值,而 fn_ref 是不确定的
lambda 使用变量时的周期中需要注意,使用拷贝时几乎就和外部没关系了,因为会拷贝出另一块新内存地址,但是要保证数据没有指针,否则指针被释放一样会崩溃
使用引用时,你需要关注使用的数据不能提起被释放,否则数据失效,因为引用的是同一块内存,通常不推荐使用引用类型的参数,因为相比拷贝,使用引用需要注意的问题更多,而且容易疏忽,所以更容易崩溃
项目路径
https://github.com/HellowAmy/mcpp.git