细讲C加加【9】C++ std::function与std::bind详解|仿函数、绑定器、类成员绑定、占位符、成员偏移指针

本篇博客基于可运行源码,完整保留源码注释与运行日志,代码块在前,知识点解析在后,自底向上梳理面试核心考点。

测试实体类 Int

cpp 复制代码
#include<iostream>
#include<functional>
using namespace std;
class Int
{
private:
	int value;	 //heap; 
public:
	Int() :value(0)
	{
		cout << "defaul create " << this << " " << value << endl;
	}
	Int(int x) :value{ x }
	{
		cout << "create Int(int) " << this << " " << value << endl;
	}
	~Int()
	{
		cout << "destroy Int : " << this << " " << value << endl;
		value = -1;
	}

	Int(const Int& it) :value(it.value)
	{
		cout << this << " copy create Int " << &it << endl;
	}
	Int& operator=(const Int& it)
	{
		if (this != &it)
		{
			this->value = it.value;
		}
		cout << this << " operator= " << &it << endl;
		return *this;
	}
	void Print() const
	{
		cout << "this: " << this << " value: " << value << endl;
	}

	Int(Int&& it) :value(it.value)
	{
		cout << this << " move create Int " << &it << endl;
		it.value = -1;
	}
	Int& operator=(Int&& it)
	{
		if (this != &it)
		{
			this->value = it.value;
			it.value = -1;
		}
		cout << this << " move operator=() " << &it << endl;
		return *this;
	}
	int& Value() { return value; }
	const int& Value() const { return value; }

	void operator()(int x) const
	{
		cout << "Int: " << x << " this:" << this << "  " << value << endl;
	}
};

void func(Int x)
{
	cout << "func(int x): " << endl;
	x.Print();
}

struct Foo {
	void operator()(Int x) const {
		cout<<"Foo value of x is "<<x.Value()<<endl;
	}
};

struct Bar {
	using pFun = void(*)(Int);
	static void func(Int x) {
		cout<<"Static value of x is "<<x.Value()<<endl;
	}
	operator pFun() const {
		return func;
	}
};

class Test {
public:
	int a_;
	double dx;
	char ch;
	int val;
public:
   Test(int x=0) : a_(x),dx(0.0),ch('a'),val(x+10) {}
   void m_func(int x) {
	   cout<<"x is "<<x<<endl;
   }
};

1.仿函数基础

仿函数是重载了()操作符的类或结构体对象,使得可以像调用函数一样调用该对象。

2.函数指针类型定义

这定义了一个名为 pFun 的函数指针类型,指向一个接受 int 参数并返回 void 的函数。

3.类静态成员函数特性

静态函数,没有所谓的this指针。静态函数属于类而非类的实例,不需要类的实例即可调用。 静态函数只能访问:同类的其他静态成员函数、同类的静态成员变量、作用域内的局部变量、全局变量(不推荐)。 特点:

  • 不依赖对象状态:没有this指针,不能访问非静态成员
  • 共享性:所有实例共享同一个静态函数
  • 生命周期:随类存在,不随对象创建和销毁
  • 访问限制:只能访问静态成员变量和函数
  • 内存占用:不会为每个对象实例创建副本

4.对象隐式转换为函数指针

这个重载允许将 Bar 类型的对象隐式转换为 pFun 类型的func函数指针,即返回func函数的指针。这个重载就是将仿函数指定为具体函数的调用。

5.类成员变量指针

我们也可以这么理解,这个定义是创建一个专门服务于Test类的一个变量,这个变量为所有的Test类服务,也就是家族的管家。 在这里它起到的作用是,储存Test类中的整形成员相对于这个类的起始地址的偏移量,并让Test类对象可以通过它访问到对应的成员变量。 因为它是为类服务的,所以只要是这个类类型的对象都可以直接去使用它,因为它本事是为Test类服务的,Test实际对象可将它当作自己中的成员变量进行访问。 只能用实例对象名+.*访问,不可以用.去查看它的偏移量,因为p并不是类的成员,管家是服务test对象,但它仍是外人,不是test类的一家人。 也不可以缺少实例对象名,因为他只知道偏移量,不知道具体类对象的起始地址。

优势 它作为一个变量,可以储存相同类型不同成员变量的偏移量,并让Test类对象通过它访问到对应的成员变量,具有强大的灵活性。

使用场景

  • 需要运行时动态访问成员
  • 实现序列化 / 反序列化框架
  • 构建反射系统
  • 实现数据驱动的配置系统
  • 需要高性能的反射机制
  • 回调机制中需要访问特定成员

6.成员变量指针与普通指针区别

指针指向类模板里面的成员,函数,不是指向已经存在的类的对象中的成员变量。

7.类成员函数指针

我们从下面的例子就知道,方法的实现有很多种,例如普通函数,成员函数,lambda表达式,函数对象,函数指针等等。 function的作用就是一个通用的多态函数包装器,它提供了强大的函数封装能力。

8.多种调用语法辨析

pfunc(x) C++写法;(*pfunc)(x) C的写法,二者实现的效果是一样的。 函数对象调用,等价于foo.operator()(x)。 函数对象调用,等价于bar.operator()(x)。


std::bind 默认值传递,会产生对象副本

cpp 复制代码
#if 0
int main() {
   Int a(10);
   cout<<"\n----------------------------------------------\n";
   auto fr1 = std::bind(func, a);
   cout<<"\n----------------------------------------------\n";
   fr1();
   cout<<"\n----------------------------------------------\n";
   return 0;
}
#endif

运行日志

cpp 复制代码
create Int(int) 012FF9FC 10

----------------------------------------------
012FF9F0 copy create Int 012FF9FC  //创建绑定器中要拷贝一份

----------------------------------------------
012FF51C copy create Int 012FF9F0 //调用绑定器,func函数传参也还要拷贝一份
func(int x) :
	this : 012FF51C value : 10
	destroy Int : 012FF51C 10

	----------------------------------------------
	destroy Int : 012FF9F0 10
	destroy Int : 012FF9FC 10

知识点

创建绑定器中要拷贝一份;调用绑定器,func函数传参也还要拷贝一份。


std::bind绑定类普通成员函数,传值与传地址对比

cpp 复制代码
class Base {
private:
	Int value;
public:
    	Base(Int x=0) :value(x) {}
		void func() {
           cout<<"Base func()"<<this<<endl;
		  
		   value.Print(); 
		   
		   value.Value()=100;

           value.Print();
		}
};
#if 0
int main() {
	Base base(20);
	std::function<void(void)> fr1 = std::bind(&Base::func, &base);//使用绑定器,传值参传的是副本,传地址,不会创建副本
	/*在C++中使用std::bind绑定类成员函数时,第一个参数必须写成 & ClassName::成员函数的形式。
	因为这里调用function的目的,就像那个家族管家一样,同类的所有对象都可以去直接使用这个公有接口直接调用func函数。
	而我们的类刚开始只是一个模板,没有具体实例,就跟一张建筑图纸一样,不在前面加一个作用域,要么系统找不到这个函数,因为没有被创建出来,
	要么就找到非类里面的同名函数,为了解决这个问题
	我们要调用这个函数,就只能去记住这个函数距离该类对象的基地址的距离,也就是偏移量
	和之前的例子:
	int Test::* p = nullptr;  
	p=&Test::a_;
	一模一样,只不过这里是一个函数指针,指向的是类成员函数,而不是成员变量。
	第二个参数就是实际类对象成员,可以是右值也可以是左值,作用就是提供基准地址,这样起始地址加上偏移量就可以找到我们想调用的类的成员方法。
	另外,由于偏移量也叫相对地址,所以要加&地址符。
	*/
	fr1();
	//调用的是副本的func函数
	cout<<"\n----------------------------------------------\n";
	base.func();
	//调用的是原对象的func函数
	return 0;
}
#endif

不加取地址符运行日志

cpp 复制代码
create Int(int) 010FF920 20
010FFA64 copy create Int 010FF920
destroy Int : 010FF920 20
010FF950 copy create Int 010FFA64
014236D8 move create Int 010FF950
destroy Int : 010FF950 - 1
Base func()014236D8
this : 014236D8 value : 20
this : 014236D8 value : 100

----------------------------------------------
Base func()010FFA64
this : 010FFA64 value : 20
this : 010FFA64 value : 100
destroy Int : 014236D8 100
destroy Int : 010FFA64 100

加取地址符运行日志

cpp 复制代码
create Int(int) 001DF818 20
001DF964 copy create Int 001DF818
destroy Int : 001DF818 20
Base func()001DF964
this : 001DF964 value : 20
this : 001DF964 value : 100

----------------------------------------------
Base func()001DF964
this : 001DF964 value : 100
this : 001DF964 value : 100
destroy Int : 001DF964 100

知识点

使用绑定器,传值参传的是副本,传地址,不会创建副本。 在C++中使用std::bind绑定类成员函数时,第一个参数必须写成 & ClassName::成员函数的形式。 因为这里调用function的目的,就像那个家族管家一样,同类的所有对象都可以去直接使用这个公有接口直接调用func函数。 而我们的类刚开始只是一个模板,没有具体实例,就跟一张建筑图纸一样,不在前面加一个作用域,要么系统找不到这个函数,因为没有被创建出来,要么就找到非类里面的同名函数,为了解决这个问题。 我们要调用这个函数,就只能去记住这个函数距离该类对象的基地址的距离,也就是偏移量。 和之前的例子:int Test::* p = nullptr; p=&Test::a_; 一模一样,只不过这里是一个函数指针,指向的是类成员函数,而不是成员变量。 第二个参数就是实际类对象成员,可以是右值也可以是左值,作用就是提供基准地址,这样起始地址加上偏移量就可以找到我们想调用的类的成员方法。 另外,由于偏移量也叫相对地址,所以要加&地址符。 fr1()调用的是副本的func函数;base.func()调用的是原对象的func函数。


std::bind占位符placeholders使用

cpp 复制代码
void funcc(int a, int b) {
	cout<<"func(int a, int b)"<<a<<","<<b<<endl;
}

#if 0
int main() {
	auto fr1 = std::bind(funcc,12,13);//使用绑定器,可以实现有参函数的无参调用
	fr1();
	auto fr2= std::bind(funcc,12,placeholders::_1);//这里的placeholders::1为占位符,
	//占位符在bind绑定器中的参数位置,决定了与函数相对应位置的形式参数绑定。
	//占位符后面的后缀,决定了使用第几个位置的实际参数传入到对应的形式参数。
	//占位符最多有20个
	fr2(13);
	auto fr3 = std::bind(funcc,placeholders::_1,13);
   	fr3(23);
	auto fr33 = std::bind(funcc,placeholders::_2,13);
    fr33(11,23);
    auto fr4 = std::bind(funcc,placeholders::_1,placeholders::_2);
    fr4(23,24);
	auto fr5 = std::bind(funcc,placeholders::_2,placeholders::_1);
    fr5(23,24);
    return 0;
}
#endif

知识点

使用绑定器,可以实现有参函数的无参调用。 这里的placeholders::_1为占位符。 占位符在bind绑定器中的参数位置,决定了与函数相对应位置的形式参数绑定。 占位符后面的后缀,决定了使用第几个位置的实际参数传入到对应的形式参数。 占位符最多有20个。


std::bind与std::function组合

cpp 复制代码
#if 0
int main() {
	// 正确使用std::bind
	auto fr1 = std::bind(funcc,12,13);//使用绑定器,可以实现有参函数的无参调用
	fr1(); // 调用bind函数

	std::function<void(void)> fx;
	fx = std::bind(funcc,12,13);
	fx();//它的实际作用是将可调用对象与其参数绑定成仿函数
	return 0;
}
#endif

知识点

使用绑定器,可以实现有参函数的无参调用。 它的实际作用是将可调用对象与其参数绑定成仿函数。


std::function回调封装类示例

cpp 复制代码
class A {
	std::function<void()> callback_;
public:
     A(const std::function<void()>& f) : callback_(f) {}
	 void setCallback(const std::function<void()>& f) {
		 callback_ = f;
	 }
	 void notify() {
		 callback_();
	 }
};

知识点

无额外注释,用于演示std::function作为回调成员的封装写法。


std::function接收仿函数对象

cpp 复制代码
#if 0
int main() {
	Foo foo;
	A a(foo);
	a.notify();
	//a.setCallback(func);
    a.notify();
    return 0;
}
#endif

知识点

无额外注释,演示仿函数传入std::function回调。


std::function赋值仿函数对象,拷贝与析构行为

cpp 复制代码
#if 0
int main() {
	std::function<void(int)> fr1;
	Int x(10),y(20);
	cout<<"---------------------------------------\n";
    fr1 = x;//赋值的过程本质上会调用拷贝构造函数,生成一个副本,实际上我们调用的是副本的仿函数。
	cout<<"---------------------------------------\n";
    fr1(10);
    cout<<"---------------------------------------\n";
	fr1 = y;//当functional对象被重新赋值时,会先调用旧对象的析构函数,会将老旧的副本进行析构,再调用新对象的拷贝构造函数。
    cout<<"---------------------------------------\n";
	fr1(20);
    cout<<"---------------------------------------\n";
	return 0;
};
#endif

运行日志

cpp 复制代码
create Int(int) 0099F718 10
create Int(int) 0099F70C 20
-------------------------------------- -
00D536CC copy create Int 0099F718
-------------------------------------- -
Int: 10 this : 00D536CC  10
-------------------------------------- -
00D53314 copy create Int 0099F70C
destroy Int : 00D536CC 10   ///原先副本被析构掉
-------------------------------------- -
Int : 20 this : 00D53314  20
-------------------------------------- -
destroy Int : 0099F70C 20
destroy Int : 0099F718 10
destroy Int : 00D53314 20

知识点

赋值的过程本质上会调用拷贝构造函数,生成一个副本,实际上我们调用的是副本的仿函数。 当functional对象被重新赋值时,会先调用旧对象的析构函数,会将老旧的副本进行析构,再调用新对象的拷贝构造函数。 原先副本被析构掉。


std::function支持的可调用对象类型

cpp 复制代码
#if 0
int main() {
	std::function<void(int)> fr1;
	Foo foo;
	fr1 = foo;//将函数对象foo赋值给fr1
	//普通函数,成员函数,lambda表达式,函数对象,函数指针等等。都可以直接赋值给它
	fr1(10);//调用方式统一
}
#endif

知识点

将函数对象foo赋值给fr1。 普通函数,成员函数,lambda表达式,函数对象,函数指针等等。都可以直接赋值给它。 调用方式统一。


std::function包装普通函数、静态成员函数

cpp 复制代码
#if 0
int main() {
	Int x(10);
	cout << "\n---------------------------------------\n";
	std::function<void(Int)> fr1 = func;
	cout << "\n---------------------------------------\n";
    fr1(x);//函数对象调用,等价于fr1.operator()(x)
	cout << "\n---------------------------------------\n";
	fr1 = Bar::func;
	cout << "\n---------------------------------------\n";
	fr1(x);
	return 0;
}
#endif

知识点

函数对象调用,等价于fr1.operator()(x)。


std::function模板参数写法

cpp 复制代码
#if 0 
int main() {
	std::function<void(int)> frl;//<>里面写的时函数去掉名字之后剩余的声明部分
	//std::function<void(*)> fr2;函数指针不可以包装。
	std::function<void* (int)> fr3;
	std::function<void*(int*)> fr4;
}
#endif

知识点

<>里面写的是函数去掉名字之后剩余的声明部分。 函数指针不可以包装。

相关推荐
良木林5 小时前
子串 - LeetCode hot 100
算法·leetcode·职场和发展
陌诺曦.6 小时前
Python扩展小练习
算法
coder!mq7 小时前
说几个常见的语法糖?
java·开发语言·算法
gugucoding7 小时前
38. 【Java】Stream API(下):高级操作与性能
java·开发语言
Lhan.zzZ7 小时前
在 Visual Studio 2022 中打造可扩展的动态链接库模块:从零搭建到原理解析
开发语言·c++·visual studio
心平气和量大福大8 小时前
android-实例-蒲公英-更新与安装-5-签名与生成APK
android·java·开发语言
码哥DFS8 小时前
构造函数、实例对象、对象原型 ----三者关系
开发语言·javascript·原型模式
不爱学英文的码字机器8 小时前
推荐算法梳理,六种主流模型与九步训练流程
算法·机器学习·推荐算法