在C++开发过程当中,函数指针非常常见,也非常强大,最广泛的用处便是作为回调函数使用,函数指针的强大之处不容小觑,我觉得它可以称为C/C++编程的灵魂之一(是不是有点儿夸张了,好吧,夸张就夸张吧)再括弧一下(其实我也不清楚这篇文章到底是应该以函数指针作为标题还是应该将回调函数作为标题,等写完再做决定吧)
首先我们来引用一下比较官方的定义(出自《C++高级编程》):
- 在C++中,可以像操作数据一样使用函数;
- 换句话说,可以获得函数的地址,然后像使用变量一样使用这个地址;
- 函数指针的类型取决于兼容函数的参数类型和返回类型
简单的说就是:函数指针就是一个地址,把函数看成一个整体,它的类型包括了参数类型和返回值类型。
普通函数指针的普通写法
先来展示一段和简单的代码
// 使用typedef定义一个函数指针类型,返回值是void,参数值是string typedef void(*processCallback)(const std::string&); // 定义一个实际的函数 void outputSomething(const std::string& content) { std::cout << "output: " << content << std::endl; } int main(){ // 相当于声明函数指针变量pc,然后使用outputSomething函数地址给它初始化赋值 // 写法一 processCallback pc = outputSomething; // 写法二 //processCallback pc = &outputSomething; // 对pc()调用相当于调用outputSomething,是不是跟基于地址操作指针变量一样的,只是这里不需要使用*解引用 pc("hello world"); system("pause"); return 0; }
上面代码注释中已经写的比较明白了,使用typedef void(*processCallback)(const std::string&);定义一个函数指针类型,注意在main函数中给函数指针变量pc赋值的两种方法是等价的,因为对于普通函数来说,可以无需“&函数名”来表示函数地址,直接使用“函数名”即可,因为聪明的编译器知道它表示函数的地址。
使用类型别名using声明来代替typedef
// 使用类型别名的方式类声明函数指针类型 using processCallback = void(*)(const std::string&); void outputSomething(const std::string& content) { std::cout << "output: " << content << std::endl; } class BackendWorker { }; int main(){ // 相当于声明函数指针变量pc,然后使用outputSomething函数地址给它初始化赋值 // 写法一 processCallback pc = outputSomething; //写法二 //processCallback pc = &outputSomething; // 对pc()调用相当于调用outputSomething,是不是跟基于地址操作指针变量一样的,只是这里不需要使用*解引用 pc("hello world"); system("pause"); return 0; }
这段代码与上面的代码几乎一致,只有第一句不同,使用using声明的方式类替代了typedef,好像比typedef的写法更加直观了一点,好像也就那样。
把函数指针作为回调函数使用
#include <iostream> #include <thread> #include <memory> #include <string> #include <chrono> #include <sstream> // 使用typedef定义一个函数指针类型,返回值是void,参数值是string //typedef void(*processCallback)(const std::string&); // 使用类型别名的方式类声明函数指针类型 using processCallback = void(*)(const std::string&); void outputSomething(const std::string& content) { std::cout << "output: " << content << std::endl; } class BackendWorker { public: BackendWorker() : m_pc_(nullptr) , m_thread_ptr_(nullptr) { } // 设置回调函数 void registerCallback(processCallback pc) { m_pc_ = pc; } // 工作函数,假设该函数做一些耗时任务,或者在其他线程当中做一些耗时任务 void run() { if (m_thread_ptr_) { if (m_thread_ptr_->joinable()) { m_thread_ptr_->join(); } } m_thread_ptr_.reset(new std::thread([this] { // 这里就直接sleep和for循环模拟耗时任务的过程 for (int i = 0; i < 5; ++i) { std::this_thread::sleep_for(std::chrono::seconds(3)); std::stringstream ss; ss << "processing " << i << "............."; if(m_pc_) m_pc_(ss.str()); } if(m_pc_) m_pc_("process finished."); })); } private: // 声明函数指针类型,用于接收回调函数的注册 processCallback m_pc_; std::unique_ptr<std::thread> m_thread_ptr_; }; int main(){ BackendWorker worker; worker.registerCallback(outputSomething); worker.run(); system("pause"); return 0; }
这里使用一个BackendWorker模拟后台工作类,在BackendWorker中有一个函数指针processCallback类型的成员变量m_pc_,用于接收用户注册的回调接口,然后在main函数中BackendWorker的实例在启动真正的工作也就是调用run函数之前,先调用registerCallback注册回调,函数指针作为回调函数类型的这种注册和调用方式,在实际工作中是很常见的。
使用更高级的std::function类模板
C++11提供了类模板std::function,该类模板的牛X之处在于其可以指向函数,函数对象,lambda表达式,可以说是任何可调用的对象,std::function类的用处很广泛,这里不展开说,可以自行去看文档或书籍,这里只说用他来替换函数指针的用法。把上面的例子稍微修改一下即可
#include <iostream> #include <thread> #include <memory> #include <string> #include <chrono> #include <sstream> #include <functional> // 使用typedef定义一个函数指针类型,返回值是void,参数值是string //typedef void(*processCallback)(const std::string&); // 使用类型别名的方式类声明函数指针类型 //using processCallback = void(*)(const std::string&); // 使用std::function声明的方式 // 这样子是不是更加清晰一点了,类模板std::function的模板参数是返回值 // 为void, 参数为const std::string&的可调用对象 using processCallback = std::function<void(const std::string&)>; void outputSomething(const std::string& content) { std::cout << "output: " << content << std::endl; } class BackendWorker { public: BackendWorker() : m_pc_(nullptr) , m_thread_ptr_(nullptr) { } ~BackendWorker() { if (m_thread_ptr_) { if (m_thread_ptr_->joinable()) { m_thread_ptr_->join(); } } } // 设置回调函数 void registerCallback(processCallback pc) { m_pc_ = pc; } // 工作函数,假设该函数做一些耗时任务,或者在其他线程当中做一些耗时任务 void run() { if (m_thread_ptr_) { if (m_thread_ptr_->joinable()) { m_thread_ptr_->join(); } } m_thread_ptr_.reset(new std::thread([this] { // 这里就直接sleep和for循环模拟耗时任务的过程 for (int i = 0; i < 5; ++i) { std::this_thread::sleep_for(std::chrono::seconds(3)); std::stringstream ss; ss << "processing " << i << "............."; if(m_pc_) m_pc_(ss.str()); } if(m_pc_) m_pc_("process finished."); })); } private: // 声明函数指针类型,用于接收回调函数的注册 processCallback m_pc_; std::unique_ptr<std::thread> m_thread_ptr_; }; int main(){ BackendWorker worker; // 注册方式1 使用普通函数 worker.registerCallback(outputSomething); // 注册方式2 使用lambda表达式 /*worker.registerCallback([](const std::string& content) { std::cout << "output: " << content << std::endl; });*/ worker.run(); system("pause"); return 0; }
using processCallback = std::function<void(const std::string&)>;这样子声明函数对象是不是更加的清晰,模板类的模板参数是void(const std::string&),这样一看就很清晰,返回值是void,参数类型是const std::string&可调用对象,这里的可调用对象可以是普通函数,lambda表达式(在main函数中的注册方式2,演示的就是lambda表达式的用法),还可以是函数对象(这里暂不演示),下面演示使用类成员函数为processCallback赋值来代替普通函数作为回调的用法
#include <iostream> #include <thread> #include <memory> #include <string> #include <chrono> #include <sstream> #include <functional> // 使用typedef定义一个函数指针类型,返回值是void,参数值是string //typedef void(*processCallback)(const std::string&); // 使用类型别名的方式类声明函数指针类型 //using processCallback = void(*)(const std::string&); // 使用std::function声明的方式 // 这样子是不是更加清晰一点了,类模板std::function的模板参数是返回值 // 为void, 参数为const std::string&的可调用对象 using processCallback = std::function<void(const std::string&)>; void outputSomething(const std::string& content) { std::cout << "output: " << content << std::endl; } class BackendWorker { public: BackendWorker() : m_pc_(nullptr) , m_thread_ptr_(nullptr) { } ~BackendWorker() { if (m_thread_ptr_) { if (m_thread_ptr_->joinable()) { m_thread_ptr_->join(); } } } // 设置回调函数 void registerCallback(processCallback pc) { m_pc_ = pc; } // 工作函数,假设该函数做一些耗时任务,或者在其他线程当中做一些耗时任务 void run() { if (m_thread_ptr_) { if (m_thread_ptr_->joinable()) { m_thread_ptr_->join(); } } m_thread_ptr_.reset(new std::thread([this] { // 这里就直接sleep和for循环模拟耗时任务的过程 for (int i = 0; i < 5; ++i) { std::this_thread::sleep_for(std::chrono::seconds(3)); std::stringstream ss; ss << "processing " << i << "............."; if(m_pc_) m_pc_(ss.str()); } if(m_pc_) m_pc_("process finished."); })); } private: // 声明函数指针类型,用于接收回调函数的注册 processCallback m_pc_; std::unique_ptr<std::thread> m_thread_ptr_; }; // 前台工作,可以想象成用户调用类,或者UI等 class FrontendWorker { public: // 模拟处理后台类处理之后回传过来的结果 // 在实际应用场景中,这里可以做一些通知用户 // 或者通知UI刷新界面等操作 void handle(const std::string& content) { std::cout << "handle output: " << content << std::endl; } }; int main(){ BackendWorker backWorker; FrontendWorker frontWorker; // 把frontWorker的成员函数handle注册为backWorker的回调 backWorker.registerCallback(std::bind(&FrontendWorker::handle, &frontWorker, std::placeholders::_1)); backWorker.run(); system("pause"); return 0; }
使用类成员函数赋值给std::function类型参数需要借用std::bind函数,第一个参数是函数地址,第二个参数必须是函数所属的对象的地址,至于函数本身的参数就紧接着其后,std::placeholders::_1在这里表示的是函数参数const std::string&是在实际调用时的第一个参数。关于std::function和std::bind的详细使用,请查看文档或者书籍,这里介绍一个C++的线上文档,在开发时候可以经常查看https://zh.cppreference.com/w/
最后说一下类成员函数函数指针
#include <iostream> #include <string> #include <functional> using processCallback = std::function<void(const std::string&)>; class FrontendWorker; using memFunc = void (FrontendWorker::*)(const std::string& content); // 前台工作,可以想象成用户调用类,或者UI等 class FrontendWorker { public: // 模拟处理后台类处理之后回传过来的结果 // 在实际应用场景中,这里可以做一些通知用户 // 或者通知UI刷新界面等操作 void handle(const std::string& content) { std::cout << "handle output: " << content << std::endl; } }; int main(){ FrontendWorker frontWorker; memFunc func = &FrontendWorker::handle; // 注意这里的调用方式与普通成员函数类型指针不一致了 (frontWorker.*func)("hello world"); // 使用std::function完全可以替代上述方式 processCallback pc = std::bind(&FrontendWorker::handle, &frontWorker, std::placeholders::_1); pc("hello world, std::function"); system("pause"); return 0; }
注意上面代码中成员函数指针赋值之后的调用方式,看起来比较难受
memFunc func = &FrontendWorker::handle; // 注意这里的调用方式与普通成员函数类型指针不一致了 (frontWorker.*func)("hello world");
这里注意几点
- 给成员函数指针变量赋值的时候,需要在类成员函数前面加上&
- 在调用函数时候需要像这样(frontWorker.*func)(“hello world”);在函数前面加上*解引用
- (frontWorker.*func)(“hello world”);中的第一个()是必须的,因为()的优先级比*高
但是main函数中我也写了使用std::function的方式代替了成员函数指针类型的方式,调用的效果完全一样,所以完全可以避开成员函数指针声明和调用这种复杂的方式,但是原理要懂。