可调用对象
在C++中,可调用对象是一个广泛的概念,涵盖了任何可以通过函数调用运算符()
调用的实体。这些对象不仅包括普通函数和类的成员函数,还包括对象,这些对象可以像函数一样调用。下面是一些C++中可调用对象的类型以及它们的使用方法:
普通函数
普通函数类型可以声明函数、定义函数指针和函数引用,但是不能使用函数类型直接定义函数的实体。
#include <iostream>
using namespace std;
using Fun = void(int, const string&); // 普通函数的别名
Fun greet; // 声明普通函数
// 普通函数声明
//void greet(int id, const string& message);
int main() {
greet(1, "Hello, World!"); // 直接调用普通函数
// 通过函数指针调用函数
void(*funcPtr)(int, const string&) = greet;
funcPtr(2, "Hello again!");
// 通过函数引用调用函数
void(&funcRef)(int, const string&) = greet;
funcRef(3, "Hello once more!");
return 0;
}
// 普通函数定义
void greet(int id, const string& message) {
cout << "Message " << id << ": " << message << endl;
}
类的静态成员函数
类的静态成员函数是属于类的函数,而不是类的某个特定实例。**它们可以在没有创建类的对象的情况下调用。**静态成员函数通常用于与类相关的功能,而不是与特定对象相关的功能。看看下面的示例:
#include <iostream>
using namespace std;
class Messenger {
public:
// 静态成员函数声明
static void greet(int id, const string& message);
};
int main() {
Messenger::greet(1, "Hello, World!"); // 直接调用静态成员函数
// 通过函数指针调用静态成员函数
void(*funcPtr)(int, const string&) = Messenger::greet;
funcPtr(2, "Hello again!");
// 通过函数引用调用静态成员函数
void(&funcRef)(int, const string&) = Messenger::greet;
funcRef(3, "Hello once more!");
return 0;
}
// 静态成员函数定义
void Messenger::greet(int id, const string& message) {
cout << "Message " << id << ": " << message << endl;
}
仿函数
仿函数的本质是类,只不过了是重载了函数调用运算符operator()
。
下面是一个使用仿函数的例子:
#include <iostream>
using namespace std;
// 定义一个仿函数类
class Greeting {
public:
void operator()(int id, const string& msg) {
cout << "Greetings " << id << ", " << msg << endl;
}
};
int main() {
Greeting greet; // 创建一个仿函数类的实例
greet(101, "Welcome to the team!"); // 通过仿函数实例调用
Greeting()(102, "Happy coding!"); // 通过匿名仿函数实例调用
Greeting& refGreet = greet; // 仿函数引用
refGreet(103, "Have a great day!"); // 通过仿函数引用调用
return 0;
}
lambda函数
lambda函数的本质是仿函数,只不过它提供了更简洁的方式以来定义局部函数对象。
#include <iostream>
using namespace std;
int main() {
// 定义一个lambda表达式,并赋值给auto变量
auto sayHello = [](int id, const string& msg) {
cout << "Hello " << id << ", " << msg << endl;
};
// 直接通过lambda对象调用
sayHello(201, "welcome to lambda functions!");
// 通过lambda对象的引用调用
auto& refSayHello = sayHello;
refSayHello(202, "lambdas are powerful!");
return 0;
}
类的非静态成员函数
类的非静态成员函数与类的对象紧密相关,所以我们必须通过类的实体才能调用这些函数。
类的非静态成员函数只有指针类型,没有引用类型,不能引用。
类的非静态成员函数有地址,在使用函数指针时,需要取地址符号&
。
#include <iostream>
using namespace std;
class MessagePrinter {
public:
void display(int id, const string& msg) {
cout << "Message " << id << ": " << msg << endl;
}
};
int main() {
MessagePrinter printer;
printer.display(21, "Hello, World!");
// 成员函数指针的声明和使用
void (MessagePrinter::*funcPtr)(int, const string&) = &MessagePrinter::display;
(printer.*funcPtr)(22, "This is a member function pointer");
// c++11中的成员函数指针
auto funcPtr2 = &MessagePrinter::display;
(printer.*funcPtr2)(23, "This is a member function pointer in c++11");
// c++11中的成员函数指针,使用using关键字
using pFun = void (MessagePrinter::*)(int, const string&);
pFun funcPtr3 = &MessagePrinter::display;
(printer.*funcPtr3)(24, "This is a member function pointer in c++11 with using");
return 0;
}
可被转换为函数指针的类对象
类可以重载类型转换运算符operator 数据类型() ,如果数据类型是函数指针或函数引用类型,那么该类实例也将成为可调用对象。
它的本质是类,调用的代码像函数。
在实际开发中,意义不大。
下面示例:不需要看
#include <iostream>
using namespace std;
// 定义一个普通函数
void displayMessage(int id, const string& msg) {
cout << "Message " << id << ": " << msg << endl;
}
// 定义一个可被转换为函数指针的类
class ConvertibleToFunctionPointer {
public:
using FunctionType = void (*)(int, const string&);
// 重载类型转换运算符
operator FunctionType() const {
return displayMessage;
}
};
int main() {
ConvertibleToFunctionPointer convert;
convert(23, "Callable like a function");
return 0;
}
包装器 function
std::function
是一个模板类,它可以用来封装任何类型的可调用对象。这个模板类的定义大致如下:
template <typename T>
class function;
其中T是一个函数类型,比如void(int)
表示一个接受int
参数并返回void
的函数。
使用时需要包含头文件<functional>
注意:
std::function
重载了bool
类型的转换操作符,这意味着你可以直接在if
语句中使用std::function
对象来检查它是否包含一个可调用对象。- 如果
std::function
对象没有包装任何可调用对象,尝试调用它将会抛出一个std::bad_function_call
异常
使用方法:
例如有一个普通的全局函数:
void greet (int id , const string & asg){
cout<< "Message " << id << ": " << asg << endl;
}
可以使用function
来包装这个函数:
function <void(int, const string&)> func = greet;
func(1, "Hello");
上面提到的如果没有给function
包装可调用对象,会抛出异常,请看示例:
function <void(int, const string&)> func;
try{
func(1, "Hello");
}catch (const bad_function_call& e){
cout << e.what() << endl;
}
下面请看完整示例:
#include <iostream>
#include <functional>
#include <string>
using namespace std;
// 普通函数
void greet(int id, const string& message) {
cout << "用户" << id << "," << message << endl;
}
// 类的静态成员函数
struct User {
static void staticGreet(int id, const string& message) {
cout << "用户" << id << "," << message << endl;
}
};
// 函数对象(仿函数)
struct GreetFunc {
void operator()(int id, const string& message) const {
cout << "用户" << id << "," << message << endl;
}
};
// 类的非静态成员函数
struct MemberFunc {
void memberGreet(int id, const string& message) {
cout << "用户" << id << "," << message << endl;
}
};
// 可以转换为函数指针的类
struct ConvertibleFunc {
using FuncType = void (*)(int, const string&);
operator FuncType() const {
return greet; // 这里假设greet是之前定义的那个全局函数
}
};
int main() {
// 包装普通函数
function<void(int, const string&)> func1 = greet;
func1(1, "欢迎来到C++的世界。");
// 包装静态成员函数
function<void(int, const string&)> func2 = User::staticGreet;
func2(2, "欢迎来到C++的世界。");
// 包装函数对象(仿函数)
GreetFunc greetFunc;
function<void(int, const string&)> func3 = greetFunc;
func3(3, "欢迎来到C++的世界。");
// 包装Lambda表达式
function<void(int, const string&)> func4 = [](int id, const string& message) {
cout << "用户" << id << "," << message << endl;
};
func4(4, "欢迎来到C++的世界。");
// 包装成员函数
MemberFunc obj;
auto memberFunc = bind(&MemberFunc::memberGreet, &obj, placeholders::_1, placeholders::_2);
function<void(int, const string&)> func5 = memberFunc;
func5(5, "欢迎来到C++的世界。");
// 包装可以转换为函数指针的类对象
ConvertibleFunc convFunc;
function<void(int, const string&)> func6 = convFunc;
func6(6, "欢迎来到C++的世界。");
// 尝试调用一个空的function对象
function<void(int, const string&)> emptyFunc;
try {
emptyFunc(7, "尝试调用空的function对象。");
} catch (bad_function_call& e) {
cout << "捕获到异常:" << e.what() << endl;
}
return 0;
}
包装器和函数指针的区别
当看完上述示例以后,可能会有个疑问,包装器function
和函数指针看起来一模一样啊,都是给函数对象起别名,然后可以用别名调用它。这里我详细说明一下包装器和函数指针的区别。
通用性:
function
是一个模板类,他能够包装几乎所有类型的可调用实体。- 函数指针只能指向普通的非成员函数。
统一语法:
function
提供了一个统一的方式来处理所有的可调用对象,无论它们的类型如何。- 函数指针的语法必须与它所指向的函数的返回类型和参数类型完全匹配。
状态和行为:
function
可以存储状态,例如,它可以包装一个绑定了特定数据的lambda表达式或者函数对象。- 函数指针时无状态的,它只代表一个函数的地址。
安全性:
function
重载了bool
运算符,可以用于检查是否包装了可调用对象。- 函数指针如果为
nullptr
并被调用,可能会导致未定义行为。
所以,在编写程序时,更加倾向于使用包装器function
。
适配器bind
std::bind
它允许你将函数、方法或者函数对象与其参数或者部分参数进行绑定,从而创建一个新的可调用对象。
使用时需要包含头文件<functional>
template <class Fx, class... Args>
function <> bind(Fx&& fx, Args&&... args)
Fx
:需要绑定的可调用对象
args
:绑定参数列表,可以是左值、右值和参数占位符std::placeholders::_n
,如果参数不是占位符,缺省为值传递,std::ref(参数)
则为引用传递。
bind
还有一个用途是改变函数参数的顺序。有时候你可能有一个函数,它接受两个参数,但是你想创建一个新的函数,这个函数接受这两个参数的相反顺序。std::bind 可以轻松实现这一点,只需将占位符按照你想要的顺序传递即可。
例如:
void print(int a, int b) { cout << "a: " << a << ", b: " << b << endl; }
原始顺序调用是
print(1, 2);
使用bind改变参数顺序
auto reversedPrint = std::bind(print, placeholders::_2, placeholders::_1);
这里
placeholders::_1
表示的绑定对象被调用时传递的第一个参数。另外,被绑定对象的参数列表有几个参数,你就要需要提供多少个占位符(假设不预先固定一些参数的值)
在使用 std::bind 时,如果参数不是占位符,它们默认是通过值传递的。如果你想要通过引用传递,你可以使用 std::ref 或 std::cref。
void increment(int& value) {
++value;
}
int main() {
int x = 0;
// 使用 std::ref 来传递 x 的引用
auto incrementX = bind(increment, ref(x));
incrementX(); // 增加 x 的值
cout << "x: " << x << endl; // 输出:x: 1
return 0;
}
最后,值得注意的是 std::bind 返回的是一个 std::function 类型的对象。这意味着你可以将绑定的结果存储在 std::function 中,或者直接使用它来调用函数。
下面请看一个示例:
#include <iostream>
#include <functional>
using namespace std;
// 普通函数
void show(int bh, const string& message) {
cout << " " << bh << "号," << message << endl;
}
int main()
{
// 直接将普通函数赋值给function对象fn1
function<void(int, const string&)> fn1 = show;
// 使用bind将普通函数show的参数绑定到占位符,创建一个可调用对象fn2
function<void(int, const string&)> fn2 = bind(show, placeholders::_1, placeholders::_2);
fn1(1, "适配器bind");
fn2(1, "适配器bind");
// 将函数show的参数顺序颠倒,并绑定到占位符,创建可调用对象fn3
function<void(const string&, int)> fn3 = bind(show, placeholders::_2, placeholders::_1);
fn3("适配器bind", 1);
// 将函数show的第一个参数固定为3,第二个参数绑定到占位符,创建可调用对象fn4
function<void(const string&)> fn4 = bind(show, 3, placeholders::_1);
fn4("适配器bind");
// bind中提供的参数比show函数多一个,但是多余的参数会被忽略,创建可调用对象fn5
function<void(int, const string&,int)> fn5 = bind(show, placeholders::_1, placeholders::_2);
fn5(8, "适配器bind", 1);
}
下面请看六种可调用对象的完整示例:
#include <iostream>
#include <functional>
using namespace std;
// 普通函数
void show(int bh, const string& message) {
cout << "编号" << bh << "," << message << endl;
}
struct AA // 类中有静态成员函数。
{
static void show(int bh, const string& message) {
cout << "编号" << bh << "," << message << endl;
}
};
struct BB // 仿函数。
{
void operator()(int bh, const string& message) {
cout << "编号" << bh << "," << message << endl;
}
};
struct CC // 类中有普通成员函数。
{
void show(int bh, const string& message) {
cout << "编号" << bh << "," << message << endl;
}
};
struct DD // 可以被转换为普通函数指针的类。
{
using Fun = void (*)(int, const string&); // 函数指针的别名。
operator Fun() {
return show; // 返回普通函数show的地址。
}
};
int main()
{
// 普通函数。
function<void(int, const string&)> fn1 = bind(show, placeholders::_1, placeholders::_2);
fn1(1, "适配器bind");
// 类的静态成员函数。
function<void(int, const string&)> fn3 = bind(AA::show, placeholders::_1, placeholders::_2);
fn3(2, "适配器bind");
// 仿函数。
function<void(int, const string&)> fn4 = bind(BB(), placeholders::_1, placeholders::_2);
fn4(3, "适配器bind");
// 创建lambda对象。
auto lb = [](int bh, const string& message) {
cout << "编号" << bh << "," << message << endl;
};
function<void(int, const string&)> fn5 = bind(lb, placeholders::_1, placeholders::_2);
fn5(4, "适配器bind");
// 类的非静态成员函数。
CC cc;
function<void(int, const string&)> fn11 = bind(&CC::show,&cc,placeholders::_1, placeholders::_2);
fn11(5, "适配器bind");
// 可以被转换为函数指针的类对象。
DD dd;
function<void(int, const string&)> fn12 = bind(dd, placeholders::_1, placeholders::_2);
fn12(6, "适配器bind");
}
可变函数和参数
用bind
绑定一个函数及其参数并执行函数,功能和thread
类的构造函数非常类似,后者允许你以类似的方式启动一个线程。
#include <iostream>
#include <functional>
using namespace std;
// 定义一个无参数的函数
void greet() {
cout << "Hello, World!" << endl;
}
// 定义一个带参数的函数
void customGreet(const string& name) {
cout << "Hello, " << name << "!" << endl;
}
// 定义一个模板函数,它接受函数对象和参数,然后执行该函数
template<typename Fn, typename... Args>
void execute(Fn&& fn, Args&&... args) {
auto boundFunc = bind(forward<Fn>(fn), forward<Args>(args)...);
boundFunc(); // 执行绑定的函数
}
int main() {
// 调用无参数的函数
execute(greet);
// 调用带参数的函数
execute(customGreet, "Alice");
return 0;
}
回调函数的实现
回调函数是一种在编程中常用的技术,尤其是在处理异步操作时非常有用。当某些时间发生时(例如,数据接收完毕),回调函数被调用以处理该事件。在消息队列和网络通信等场景中,这种机制特别常见。下面将详细讲解在C++中如何实现回调函数,特别是在一个简单的消息队列模型中。
首先,我们用之前学过的生产消费模型来实现一个消息队列,用于存储和传递消息。
接着我们顶一个回调函数的接口。用function
包装。
首先我们来定义消息处理函数和类:
#include <iostream>
#include <string>
#include <functional>
using namespace std;
void handleData(const string& message) {
cout << "处理数据:" << message << endl;
}
class DataHandler {
public:
void handleData(const string& message) {
cout << "处理数据(类成员):" << message << endl;
}
};
接下来,定义 消息队列类:
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
class MessageQueue {
mutex m_mutex;
condition_variable m_cond;
queue<string> m_q;
function<void(const string&)> m_callback;
public:
// 注册回调函数。当消息到来时,将调用此函数。
void registerCallback(const function<void(const string&)>& callback) {
m_callback = callback;
}
// 生产数据函数。外部调用此函数来添加消息到队列。
void produceData(const string& data) {
unique_lock<mutex> lock(m_mutex);
m_q.push(data);
m_cond.notify_one(); // 通知一个等待的消费者线程
}
// 消费数据函数。线程将运行此函数来消费消息。
void consumeData() {
while (true) {
unique_lock<mutex> lock(m_mutex);
等待条件变量,直到队列非空
m_cond.wait(lock, [this] { return !m_q.empty(); });
string message = m_q.front();
m_q.pop();
lock.unlock();
if (m_callback) { // 如果设置了回调函数
m_callback(message); // 调用回调函数处理消息
}
}
}
};
最后在main函数中使用这个模型:
int main() {
MessageQueue queue; // 创建 MessageQueue 的实例
// 注册普通函数作为回调
queue.registerCallback(handleData);
// 注册类成员函数作为回调
DataHandler handler;
queue.registerCallback(bind(&DataHandler::handleData, &handler, placeholders::_1));
thread consumerThread(&MessageQueue::consumeData, &queue); // 创建消费者线程
this_thread::sleep_for(chrono::seconds(1)); // 等待一段时间
queue.produceData("Hello, World!"); // 生产消息
this_thread::sleep_for(chrono::seconds(1)); // 再等待一段时间
queue.produceData("Another message"); // 生产另一条消息
consumerThread.join(); // 等待消费者线程结束
return 0;
}
在这个例子中,我们使用了std::function
来存储回调函数,使用了std::bind
来绑定类成员函数作为回调。通过这种方式,我们可以灵活地处理接收到的消息。
如何取代虚函数
C++虚函数在执行过程时会有额外的成本,因为它们涉及到动态绑定,即在运行时决定调用哪个函数。这是多态性的基础,在C++中通过虚函数表(vtable)来实现。每个具有虚函数的类都有一个虚函数表,这是一个函数指针数组,其中存储了指向类的虚函数的指针。当通过基类指针或引用调用虚函数时,会有以下步骤:
- 查找对象的虚函数表(vtable):这需要一次内存访问。
- 通过虚函数表找到真正的虚函数地址:这是另一次内存访问
- 最后跳转到函数实际地址并执行。
相比之下,非虚函数(普通函数)是静态绑定的,可以一次直接调用,开销比虚函数调用要小。
为了避免虚函数的开销,通过function
和bind
,可以将子类的成员函数注册到基类中,然后通过基类调用这些函数,从而模拟了虚函数的行为,但没有使用动态绑定。
#include <iostream>
#include <functional>
using namespace std;
struct Vehicle { // Vehicle基类
function<void()> m_callback; // 用于绑定子类的成员函数。
// 注册子类成员函数,子类成员函数没有参数。
template<typename Fn, typename ...Args>
void registerCallback(Fn&& fn, Args&&...args) {
m_callback = bind(forward<Fn>(fn), forward<Args>(args)...);
}
void move() {
if (m_callback) {
m_callback(); // 调用子类的成员函数。
}
}
};
struct Car : public Vehicle { // Car派生类
void drive() { cout << "Car is driving.\n"; }
};
struct Bike : public Vehicle { // Bike派生类
void ride() { cout << "Bike is riding.\n"; }
};
int main() {
// 根据用户选择的交通工具,进行移动。
int choice = 0;
cout << "请选择交通工具(1-汽车;2-自行车):";
cin >> choice;
Vehicle* vehiclePtr = nullptr;
if (choice == 1) { // 1-汽车
vehiclePtr = new Car;
vehiclePtr->registerCallback(&Car::drive, static_cast<Car*>(vehiclePtr)); // 注册子类成员函数。
} else if (choice == 2) { // 2-自行车
vehiclePtr = new Bike;
vehiclePtr->registerCallback(&Bike::ride, static_cast<Bike*>(vehiclePtr)); // 注册子类成员函数。
}
if (vehiclePtr != nullptr) {
vehiclePtr->move(); // 调用子类的成员函数。
delete vehiclePtr; // 释放派生类对象。
}
}