admin管理员组

文章数量:1794759

初识C++ · C++11(3)

前言:

本文介绍的是包装器以及线程库的简单了解,但是呢,线程是基于对Linux有一定的了解,所以本文就是简单介绍一下,介绍完包装器以及线程库的简单理解之后C++11的特性就到此为止,当然C++11远不止于此!

1 包装器

先来了解包装器的大体分类,分为两种,一种是类模板包装器,一种是函数模板包装器,说是两大类,今天介绍两个,一个是function包装器,一个是bind包装器。

1.1 function

在学习function之前,我们先来了解一下什么是可调用对象?

可调用对象就是指可以实例化的并且可以调用的对象呗: 仿函数是吧?但是缺点是要实例化出多个类,并且类型不太好控制,比如面对自定义类型的操作。

函数指针是吧?但是C++不太喜欢使用。

lambda表达式是吧?但是lambda表达式没有类型概念,所以每次需要auto接受,相对于其他两个来说,lambda表达式算是比较好操作的了。

但是呢,就像列表初始化一样,包装器可能就有点像大一统,希望面对不同场景的时候都可以使用一种方式解决,所以,包装器就出场了,包装器其实就是对上面的可调用对象的一个封装而已。

先看一个没有使用包装器的场景:

代码语言:javascript代码运行次数:0运行复制
template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

使用了三种不一样的可调用对象,乍一看好像没有问题,但是运行的时候可以发现useF函数实例化出了三份,这在模板学习的时候就提及了,是编译器在负重前行,那么现在我们希望给编译器减少一点负担,即实例化出一个就行,预期结果是打印出来的count的地址都是一样的,并且个数为1.

此时可以使用function了,function的头文件位于functional中,定义如下:

看着是有点看不懂吧,咱们直接使用一下:

代码语言:javascript代码运行次数:0运行复制
struct Functor
{
	int operator()(const int& a, const int& b)
	{
		return a + b;
	}
};

int f(const int& a, const int& b)
{
	return a + b;
}


int main()
{
	function<int(int, int)> f1 = Functor();
	function<int(int, int)> f2 = f;
	function<int(int, int)> f3 = [](const int a, const int b) {return a + b; };
	
	f1(1, 2);
	f2(3, 4);
	f3(5, 4);

	return 0;
}

语法看起来有点怪的,最外层的尖括号里面是返回值和参数,圆括号括起来的是函数参数,外面的是函数返回值,注意,这里需要保持基本类型一致,比如前面写f2 = f,f函数的基本数据类型是Int,所以说包装器function的基本类型也是三个int。

那么怎么使用function解决类模板实例化多份的问题呢?

代码语言:javascript代码运行次数:0运行复制
template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	
	function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	
	function<double(double)> func3 = [](double d)->double 
		{
			return d /4; 
		};
	cout << useF(func3, 11.11) << endl;

	return 0;
}

简单呀,包装一下就可以了,但是为什么经过包装了之后就可以完成只实例化一个对象呢?

前面实例化多份对象的原因是因为模板参数接受的参数不一样,函数指针啊 匿名对象啊 lambda表达式啊,但是最终完成的操作都是实现除,所以就实例化了多份对象。

经过function包装了之后,参数都是一样的,就不存在说一定要经过函数模板再去调用了,直接就调用了。

以上是function的基本用法,那么现在进阶一下。

上面的包装都是包装一整个类或者是函数对象等,如果一个类有多个成员函数,我们只想要包装其中一个成员函数呢?

这里就要分为静态成员函数和非静态成员函数了。

首先来看一下静态成员函数的包装:

代码语言:javascript代码运行次数:0运行复制
class Master
{
public:
	static int Mass(int a, int b)
	{
		return a + b;
	}
	int Masn(int a,int b)
	{
		return a + b;
	}
};

int main()
{
	//&符号可以加可以不加
	function<int(int,int)> f1 = &Master::Mass;
	f1(2,2);

	return 0;
}

因为是静态函数,所以需要类域来访问,这里的语法就是这样的,对于&符号来说加不加都是可以的,重点在于非静态成员函数:

代码语言:javascript代码运行次数:0运行复制
class Master
{
public:
	static int Mass(int a, int b)
	{
		return a + b;
	}
	int Masn(int a,int b)
	{
		return a + b;
	}
};

int main()
{
	//&符号可以加可以不加
	function<int(int,int)> f1 = &Master::Mass;
	f1(2,2);

	//&符号必须加
	function<int(Master*,int, int)> f2 = &Master::Masn;
	Master m1;
	f2(&m1, 1, 2);

	function<int(Master,int,int)> f3 = &Master::Masn;
	f3(Master(),1, 1);

	return 0;
}

先看语法使用,语法使用如上。

首先,取地址符号是一定要加的,其次,域名访问限定符也是要加的,那么为什么要传类类型的指针或匿名对象呢?

思考一个问题,非静态成员函数的参数有多少个?这里要特别注意的是,除了显式的两个int,还有this指针!所以,为什么保持参数一致,我们就应该传类类型的指针。

但是为什么传匿名对象也可以呢?思考一个问题,参数传给的是funtion吗?参数是传给类对象的,然后通过类对象调用函数,funtion只是起到了一个包装的作用,实际上的参数调用还是要通过类对象来实现,那么函数由谁调用,由函数指针,或者是函数对象调用,所以这里包装非静态成员函数的实质还是要通过对象来调用函数,所以两种传值的方式都是可以的。


1.2 bind

bind属于funtional里面的Functions部分,function属于classes部分,这也说明了它们一个是类模板包装器,一个是函数模板包装器。

那么bind的作用是什么呢?

bind一般有两个作用,一个是调整参数的顺序,一个是调整参数的个数,其中调整顺序一般不太用,毕竟用处没那么大,调整参数的个数是很有用的。

但是要注意,这里的调整参数个数不是删除某个参数或者是添加参数什么的,这样干的话已经破坏了函数本身了,这里的调整参数,比如让参数从4个到3个的意思是将其中的一个参数固定,就像缺省值那样,你不用传值,函数调用的时候已经固定了要传这个值,这是调整参数个数的本质。

调用bind的一般形式:auto newCallable = bind(callable,arg_list),其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

而_1 _2所在的命名空间是在placeholders里面,该命名空间同样也在functional里面,对应的1和2就是参数的顺序。

代码语言:javascript代码运行次数:0运行复制
int Sub(int a, int b)
{
	return a - b;
}
int main()
{
	//正常调用
	auto f1 = Sub;
	f1(1, 2);
	//未调整顺序 -> bind
	auto f2 = bind(Sub, placeholders::_1, placeholders::_2);
	cout << f2(10, 5) << endl;
	//调整顺序 -> bind
	auto f3 = bind(Sub, placeholders::_2, placeholders::_1);
	cout << f3(10, 5) << endl;

	return 0;
}

根据bind的参数,第一个参数是可调用对象,后面的是参数列表,所以第一个参数传的就是可调用对象,调整参数顺序就是调整_1 _2的位置,可以理解为_2 代表的是第二个参数,所以_1 _2交换位置就是调整参数的顺序了,这里呢,多参数也是一样的,都可以进行相应的顺序调整。

这里需要注意的是,如果是对一个类的话,是不能传一整个类的,只能传类的某个函数。

接下来是参数个数的调整,说白了,就是固定参数:

代码语言:javascript代码运行次数:0运行复制
class Sub
{
public:
	Sub(int x)
		:_x(x)
	{}

	int sub(int a, int b)
	{
		return (a - b) * _x;
	}

private:
	int _x;
};
int main()
{
	
	auto f2 = bind(&Sub::sub, placeholders::_1, placeholders::_2,placeholders::_3);
	cout << f2(Sub(1), 10, 5) << endl;
	Sub sub(1);
	cout << f2(&sub, 10, 5) << endl;

	auto f3 = bind(&Sub::sub, &sub, placeholders::_1, placeholders::_2);//参数的顺序是相对的
	cout << f3(10, 5) << endl;

	return 0;
}

对于类来说,bind的时候,语法规定需要域名访问限定符和取地址符号,那么调用的时候,同function一样,选择指针调用或者是对象调用,所以可以传匿名对象,也可以传地址过去。

f3就是一种固定参数,因为成员函数的参数列表第一个就是this指针,所以第一个参数固定的话就是sub的地址,那么为什么后面是_1 _2呢?因为第一个参数固定了,相当于只有两个参数了,所以说是相对的。

对于固定参数来说,可以选择固定任意的,比如可以固定第二个参数,其他参数不固定的话顺序和上面的一样的。 至此,引入一个让人意想不到的事实:

包装器的底层,同样是仿函数

 00007FF6CB3BF0C1  call  std::_Binder<std::_Unforced,int (__cdecl Sub::*)(int,int) __ptr64,std::_Ph<1> const & __ptr64,std::_Ph<2> const & __ptr64,std::_Ph<3> const & __ptr64>::operator()<Sub,int,int,0> (07FF6CB3B191Fh)

第一个是function的底层,第二个是bind的底层,不太好截图。

很神秘吧! 

感谢阅读!->因为作者对于线程的理解确实不够,所以有关线程的介绍留在后面,嘿嘿。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2024-08-13,如有侵权请联系 cloudcommunity@tencent 删除对象函数指针c++int

本文标签: 初识CC11(3)