C++11——新语法,新特性(怒肝三万七千字,详细解答)

目录

1.C++11 (前言)

2.列表初始化

2.1 {}的大一统

2.2 C++11中的std::initializer_list

3.右值引用和移动语义

3.1 左值和右值

3.2 左值引用和右值引用

3.3 引用延长生命周期

3.4 左值和右值的参数匹配

3.5 右值引用和移动语义的使用场景

3.5.1 移动构造和移动赋值

3.5.1.1 移动构造

 3.5.1.2 移动赋值

3.5.2 右值引用和移动语义在容器插入中的提效 

3.6 类型分类

​编辑3.7 引用折叠

3.8 完美转发

4 可变参数模板

4.1 基本语法以及原理

4.2 包扩展

 4.2.1 可变参数包的第一个用法

4.2.2 可变参数包的第二个用法

4.2.3 可变参数包的第三个用法

4.3 emplace系列

5. 新的类功能:

5.1 默认的移动构造和移动赋值

5.2 成员变量声明时给缺省值

5.3 defult和delete

5.4 final与override

6.STL中的一些变化

7.lambda

7.1 lambda表达式语法

7.2 lambda捕捉列表

 7.3 lambda的原理

7.4 lambda的应用

8.包装器

8.1 function

8.2 bind

9.结尾语


1.C++11 (前言)

其实在C++11之前,也就是只有C++98,C++03 这样的小版本进行小的改动,直到C++11,才进行了一个比较大的改动,增加了不少锦上添花的特性,也增加了不少雪中送炭的新语法。下面就跟着作者来一探究竟吧。

2.列表初始化

2.1 {}的大一统

{}初始化也叫做列表初始化。

C++11中的{}的基本思想就是,实现大一统,即所有的(内置类型,自定义类型,自定义类型本质是类型转换,中间会产生临时对象,最后优化 了以后变成直接构造。)初始化,全部都可以使用{}来初始化。在C++98的时候,也就只有数组以及结构体可以用{}来初始化。

先来看代码,通过代码理解:

struct Point
{
	int _x;
	int _y;
};

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
	//拷贝构造
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// C++98支持的
	//即当时只支持结构体和数组初始化采用花括号
	int a1[] = { 1, 2, 3, 4, 5 };
	int a2[5] = { 0 };
	Point p = { 1, 2 };

	// C++11支持的
	// 内置类型支持,内置类型都可以这么初始化
	int x0 = 1;
	int x1 = { 2 };
	//好像是带{},都可以省略"="不写。
	int x2{ 2 };

	// 自定义类型支持
	Date d0(2025, 5, 19);

	// 构造+拷贝构造->优化直接构造
	Date d1 = { 2025, 5, 19 };//多参数的隐式类型转换,拿2025,5,19去构造一个日期类的临时对象
	//之后再进行拷贝构造(因为拷贝构造只可以是相同类型之间进行)

	Date d3 { 2025, 5, 19 };//多参数隐式类型转换可以将那个等号去掉,但是单参数不可以

	//Date d2 = 2025;//单参数的隐式类型转换

	Date d2 { 2025 };
	//C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便
	//利,如容器push / inset多参数构造的对象时,{}初始化会很⽅便
	vector<Date> v;
	// 使用场景
	//下面两个就是匿名对象的使用了,也就是隐式类型转换的使用场景
	//因为明明可以写成v.push_back(d1);但这样写还得再进行构造Date临时对象,不如直接用匿名对象来的直接
	v.push_back(2025);
	v.push_back({ 2025, 5, 19 });

	// 这里d2引用的是{ 2024, 7, 25 }构造的临时对象(临时对象具有常性)
	const Date& d4 = { 2024, 7, 25 };

	// 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}
	Date d3 = { 2025 };
	Date d4 = 2025;
	Date d5{ 2025 };
 
	//{}初始化的过程中,可以省略掉=

	Point p1{ 1, 2 };
	int x2{ 2 };
	Date d6{ 2024, 7, 25 };
	const Date& d7{ 2024, 7, 25 };
 
	// 不⽀持,只有{}初始化,才能省略=
	// Date d8 2025;这个就不可以
	Date d8{ 2025 };//这个可以
	return 0;
}

大家可以根据以上的代码进行阅读,以及一些重要点,我也写在了代码中。 

2.2 C++11中的std::initializer_list

大家看这个容器vector以及list,其中的构造函数都有一个使用initializer_list进行初始化,这其实是C++11新引入的语法。

那有人就问了,咱们还是使用上面的初始化方式不就好了?不不,有区别的,区别在下面的代码的地方会讲解。

//C++的设计哲学:对固定结构使用简单机制,对动态需求提供灵活支持。
int main()
{
	/*为什么vector需要initializer_list而Date不需要?
	Date的字段数量是固定的(年、月、日),所以直接传参数或聚合初始化即可。
	vector的元素数量是动态的,需要一种机制支持可变长度的初始化。
	 C++11引入的std::initializer_list就是为了解决这个问题。
	语法支持不一样 1-3个参数初始化(只能是1到3个参数,参数个数有限制,因为上面的构造就是三个参数)
	但是下面的initializer_list就没有参数个数限制了*/
	Date d1 { 2025, 5, 19 };


	// 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的{x1,x2,x3...}
	//进行初始化。STL中的容器⽀持任意多个值构成的{x1,x2,x3...}进⾏初始化,
	// 就是通过std::initializer_list的构造函数⽀持的。
	
	// 任意多个参数初始化,这个类的本质是底层开⼀个数组,将数据拷⻉过来,
	// std::initializer_list内部有两个指针分别指向数组的开始和结束。
	vector<int> v1 {1, 2, 3, 4, 5, 6 };
	vector<int> v2 { 1, 2, 3, 4, 5, 6,7,8,9 };

	auto il1 = { 10, 20, 30 };
 
	/*所以说由下面的这个也可以看出来,{1, 2, 3, 4, 5, 6 }本质是initializer_list<int>类型
	之后再隐式类型转换为vector<int>类型,再进行拷贝构造即可(拿{1, 2, 3, 4, 5, 6 }去构造一个
	vector<int>类型的临时对象,之后再拷贝构造)*/
	initializer_list<int> il2 = { 10, 20, 30,5,5,5,5,5,5,5};
	cout << typeid(il1).name() << endl;//通过这个可以看出il1的类型

	vector<Date> v3 = { d1,d2,d3 };//有名对象(假设有d1,d2)
	vector<Date> v3 = { {2025,5,19},{2025,5,29},{2025,6,19} };//匿名对象,相当于隐式类型转换了2次

	// {}列表中可以有任意多个值
	// 这两个写法语义上还是有差别的,第⼀个v1是直接构造,
	// 第⼆个v2是构造临时对象+临时对象拷⻉v2 +优化为直接构造
	vector<int> v1({ 1,2,3,4,5 });
	vector<int> v2 = { 1,2,3,4,5 };
	const vector<int>& v3 = { 1,2,3,4,5 };


	// 这里是pair对象的{}初始化和map的initializer_list构造结合到一起用了
	map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };

	const vector<int>& v4 = { 1,2,3,4,5 };//这就是拷贝成临时对象
	// initializer_list版本的赋值支持
	v1 = { 10,20,30,40,50 };

	// STL的容器都支持initializer_list版本的构造

	return 0;
}

以上就是关于新特性 initializer_list的讲解。那么这个时候,就会有人问了,这个 initializer_list的底层是开一个数组,那么这个数组是开在栈上的?还是开在堆上的?还是开在常量区的?那么下面咱们通过几个例子来看一下:

int main()
{
	auto il1 = { 10, 20, 30 };
	initializer_list<int> il2 = { 10, 20, 30,5,5,5,5,5,5,5 };
	cout << typeid(il1).name() << endl;//这个可以查看类型

	int a[10] = { 10, 20, 30 };
	cout << a << endl;

	cout << il1.begin() << endl;//initializer_list的底层是在栈上开一个数组,从这里就可以看出数组是开在栈上的
	//因为离常量区的地址有点远,离栈区的地址很近
	const char* str = "xxxxxxxxxxxxx";
	cout << (void*)str << endl;

	//所以这个其实也支持范围for,因为底层有两个指针嘛,可以遍历数组
	/*vector(initializer_list<T> l)
	{
		for (auto e : l);
		push_back(e);
	}*/
	return 0;
}

可以看出,通过initializer_list的begin(),可以看出底层的地址与栈区的地址差不多。则initializer_list的数组就是开在栈区的。

3.右值引用和移动语义

3.1 左值和右值

左值:左值是⼀个表示数据的表达式(如变量名或解引用的指针),⼀般是有持久状态,存储在内存中,我 们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。

右值:右值也是⼀个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

简单来说,就是一个可以取地址(左值),一个不可以取地址(右值)。

// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';

cout << &c << endl;
cout << (void*)&s[0] << endl;

int*& r1 = p;
char& r2 = s[0];

// 右值:不能取地址:字面量常量,临时对象,匿名对象
//临时对象,匿名对象的生命周期都在当前这一行
double x = 1.1, y = 2.2;
// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
10;
x + y;
fmin(x, y);
string("11111");

//cout << &10 << endl;
//cout << &(x+y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("11111") << endl;

OK,再来看下面的内容:

3.2 左值引用和右值引用

其实在C++11之前,是没有左值与右值之分的,只有左值,那么自然,起别名的话,也就只有&,这一个符号,就是给左值起别名。那么现在C++11引入了右值,自然也就有了给右值起别名,那就是&&。

  // 左值引⽤给左值取别名:&

	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];
  
	//左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值

	const int& r5 = 10;
	const double& r6 = x+y;
	// 右值引用给右值取别名:&&
	int&& rr1 = 10;
	double&& rr2 = x+y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");

	
//右值引用不可以直接引左值,但是可以在左值前面加上move,即move(左值),即可以实现右值引用可以引用左值了
// 
// 但是还是不要轻易使用move,因为咱们知道左值是有资源的,move相当于你把这个左值中的资源全部转走了
// 那这个左值就悬空了,就很危险了。
// 就这样比如吧,对于深拷贝的类型,在C++11之前(没有引入右值之前),临时对象与真正的左值都是通过
// 左值引用的拷贝构造以及拷贝复制来实现资源的转移的。但是临时对象这样做就有点浪费了
// 所以就引入了移动构造与移动赋值,直接将你的资源交换给它即可。不需要再开空间另外拷贝,释放空间了
// 相当于右值是生病了的人的器官捐献,左值是好的人,而move是好的人的器官捐献。生病的人的器官捐献
// 倒是没啥,因为他马上要嘎了,但是好的人一旦器官全捐献完了(move),那他整个人不很危险了?
// 
// 并且只有深拷贝才有写移动构造的意义,因为只有深拷贝才是转移资源
// 浅拷贝写移动构造就没有任何意义。
	int&& rr5 = move(b);
	string&& rr6 = move(s);
	// move底层本质跟这里类似,就是强转
	string&& rr7 = (string&&)s;

那么这里涉及到几个问题,我需要给大家说一下:

1.这个move是什么?

move是库里面的⼀个函数模板,本质内部是进行强制类型转换,当然他还涉及⼀些引用折叠的知 识,这个我们后面会细讲。 (这个引用折叠就是T&&)。其实这个也叫做完美引用(后面会讲)。

大家可以理解为,有了move,就可以将左值强转为右值。(但是这个是有代价的,可以看我代码中的注释,所以咱们还是轻易不使用)。

2. 不是说const左值引用可以引用右值 ,为什么呢?不是说右值在绑定的时候会产生一个临时变量,而临时变量具有常性,那么为什么代码中的右值引用没有加const呢?

这个其实说来话长:在C++98的时候,那个时候只有左值,所以规定,左值的引用可以引用普通的对象。但是对于临时对象,必须要加const,否则无法绑定,这也就导致了,“临时变量具有常性这句错误的话的出现”。这其实是历史的遗留问题。那么到了C++11引入了右值这个概念之后,绑定临时变量,只需要在右值加上&&即可,这个时候不需要再加const。那么对于左值引用,还是保留了C++98的语法,即必须得加上const才可以左值引用,绑定右值。这也就是为什么右值绑定临时变量不需要加const,而左值引用临时变量必须要加const的原因。

那么这个时候又有人问了,那如果我非要在这个前面加上const呢?就是const double&& rr3 = fmin(x, y)。咱们下面讲了,这个时候的rr3是可修改的,那你加上const,无非就是不可修改的呗。但是一般这种,不常用。

3.这里还需要注意一个点:需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变 量表达式的属性是左值。

这个怎么理解呢?double&& rr3 = fmin(x, y);咱们就以这个为例子。咱们知道临时变量的生命周期就在当前这一行对吧。并且临时变量(右值)是不可以被修改的。但是一旦取了别名之后,就是rr3,那么 fmin(x, y)这个右值,就会退化左值rr3,最显著的特征就是rr3是可以修改的,并且fmin(x, y)的生命周期就是rr3的生命周期。生命周期也延长了。

4.语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1 汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是 背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。

3.3 引用延长生命周期

右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。

 int main()
 {
 std::string s1 = "Test";
 //  std::string&& r1 = s1;//错误:不可以绑定到左值

 const std::string& r2 = s1 + s1;   //可以的:到const 的左值引⽤延⻓⽣存期  
    //  r2 += "Test";              //错误:不能通过到const 的引⽤修改
 
                  
std::string&& r3 = s1 + s1;        // OK:右值引⽤延⻓⽣存期
 

 r3 += "Test";                       // OK:能通过到⾮ const 的引⽤修改
 
                 
std::cout << r3 << '\n';
 return 0;
 }

3.4 左值和右值的参数匹配

1.C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。

2.C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会 匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。

void f(int& x)
{
	std::cout << "左值引用重载 f(" << x << ")\n";
}

void f(const int& x)//所以你这里的参数加上const,则是你既可以传左值,也可以传右值,这个形参都可以接收
 //传左值:权限的缩小。传右值:就是传右值
{
	std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}

void f(int&& x)
{
	std::cout << "右值引用重载 f(" << x << ")\n";
}

int main()
{
 //有匹配的,他们会先调用最匹配的
	int i = 1;
	const int ci = 2;
	f(i); // 调用 f(int&),如果没有 f(int&) 重载则会调用 f(const int&)
	f(ci); // 调用 f(const int&)
	f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
	f(std::move(i)); // 调用 f(int&&)


	return 0;
}

好,前面的铺垫完了。那么下面要开始进行,C++11引入的雪中送炭的重要的部分:

3.5 右值引用和移动语义的使用场景

//左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实
//参和修改返回对象的价值。(即传引用返回以及传引用传参)
// C++98这里的传值返回拷贝代价就太大了,C++11之后效率就很高,不用担心效率
vector<vector<int>> generate(int numRows) {
	vector<vector<int>> vv(numRows);
	for (int i = 0; i < numRows; ++i)
	{
		vv[i].resize(i + 1, 1);
	}
	for (int i = 2; i < numRows; ++i)
	{
		for (int j = 1; j < i; ++j)
		{
			vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
		}
	}
	return vv;
}

咱们先来看着一段代码,这段代码在C++11中,这样写,完全可以。但是你若是在C++11之前这么些代码,可能会被老板骂死,根本没人像你这么写代码,因为这么写代码,你最后的传值返回,拷贝需要的代价巨大,而且你这里还是两个vector嵌套。(因为你的传值返回,得先构造一个临时对象,将数据返回值先存到临时对象中,这是一次拷贝吧。然后函数栈帧销毁,之后,还得再将临时对象中的数据拷贝到你要传的那个对象中,这又是一次拷贝吧。核算下来,两次拷贝加上两次析构。代价极大)。那这时候就有人要问了:我用传引用返回不就可以了?不可以,使用传引用 返回的前提是,你传的那个变量,不可以在函数内部定义,否则就访问野指针了。这里很明显,vv实在函数内部定义的。所以不可以使用传引用返回。而C++11引入了右值,以及移动语义。就很nice了,下面就会讲为什么很nice了。

3.5.1 移动构造和移动赋值

3.5.1.1 移动构造

1.移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引 用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。

2. 移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函 数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。

3. 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要“窃取”引用的 右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。

namespace kkk
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str)-构造" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 拷贝构造(深拷贝)
		//左值是有自己的一片空间,所以你要是想拷贝构造它,你就只能在开一片新空间
		//然后老老实实的将原空间的数据拷贝过去
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;
			reserve(s._capacity);
			for (auto ch : s)
			{
				push_back(ch);
			}
		}

		// 移动构造
		// 右值,通常是临时对象,匿名对象
		//右值既然都是马上要嘎的值了,那要是再像左值一样,开空间,深拷贝,代价就有点大了
		//不如直接把你的资源给我吧,直接调用这个里面的swap函数即可(交换两个成员对象)
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			this->swap(s);
		}
		//这里大家有没有想过一个问题,就是右值是不可以被修改的吧。那么为什么这里的“右值”s
		//还可以被修改呢?它的资源为什么还可以被转移呢?其实:需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,
		// 右值引⽤变量变量表达式的属性是左值:右值不可以被修改,
		//string && s=string("11111");
		//此时的s就是变量表达式,它是可以被修改的。所以,上面的s才可以被修改,所以swap(s)
		//之后,下面的swap的ss才可以修改。这就是为了迎合右值的资源是可以被转移的。

		void swap(string& ss)
		{
			::swap(_str, ss._str);
			::swap(_size, ss._size);
			::swap(_capacity, ss._capacity);
		}
		//bit::string&& r2 = bit::string("111111");
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;
				reserve(s._capacity);
				for (auto ch : s)
				{
					push_back(ch);
				}
			}
			return *this;
		}

		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			this->swap(s);

			return *this;
		}

		~string()
		{
			cout << "~string() -- 析构" << endl;
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				if (_str)
				{
					strcpy(tmp, _str);
					delete[] _str;
				}
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity *
					2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}

		size_t size() const
		{
			return _size;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
	//这里如果只有拷贝构造,那么str出了函数栈帧就会销毁,所以说会先将str拷贝构造到一个临时对象中
	//之后再将临时对象中的内容再拷贝构造回你main函数中接受这个函数的返回值的变量中。
	//这里需要进行两个拷贝构造,代价有点大

	//但是如果说既有移动构造又有拷贝构造,那么这里从str到临时对象,编译器会认为str是即将快嘎的
	//所以将str认为是右值,所以说这里就是移动构造,那么从临时对象再到main函数中接受这个函数的返回值的变量中
	//临时对象自然也是右值,所以这里也是移动构造,所以说两次移动构造的代价可比两次拷贝构造的代价小很多
	//这就是为什么C++98之前这个代码代价很大,C++11之后,这个代码代价就没有这么大了

	//那么会有人问,不可以用传引用返回嘛?不可以,这里str是局部变量,出了函数就销毁了,如果这里引用
	//会导致野指针的访问

	//这个函数里面,参数的传递,C++11:实参先是拷贝给一个临时对象,之后临时对象再移动·构造给形参
	//至于形参为什么不用&&,那是因为这只是类似于(值传递),因为&&一般用了之后两个的地址一样
	//这里应该是感觉没必要使用&&

	//还有一个移动构造是在返回值的时候,使用移动构造返回值(传值返回)(右值引用移动语义在
	//传值返回值的效率提升)
	string addStrings(string num1, string num2)
	{
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}

		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		cout << "******************************" << endl;
		cout << &str << endl;
		return str;
	}
	//1.这里一般在2019 debug及之前,都是将这个值返回,合二为一,有两个移动拷贝的,就合成一次移动拷贝
	// 有两次拷贝构造的,就合成一次拷贝构造。甚至拷贝+移动/拷贝构造,直接优化为构造
	// 2.vs2019 release及以后:更夸张,直接跨行优化,直接将两次全部优化没。就是str是ret的别名
	// 就已经极端到这种程度了。直接修改str就是修改ret。因为反正str最后还要传给ret
	// 所以编译器就想着不如直接来个全部优化。当然,这个优化只是编译器自己优化的,并不是所有的
	// 编译器都有这样的优化的。只有比较新的编译器才会这样搞。
	// 并且这样搞了后,就算是浅拷贝(就是也需要将数据拷贝),也会直接优化完了。
	//bit::string ret = bit::addStrings("11111", "222222222");
}

int main()
 {
    kkk::string s1("xxxxx");
    // 拷⻉构造
 
    kkk::string s2 = s1;
    // 构造+移动构造,优化后直接构造
 
    kkk::string s3 = bit::string("yyyyy");
    // 移动构造
 
    kkk::string s4 = move(s1);
    cout << "******************************" << endl;
    
    return 0;
 }

其实来看两张图就可以理解,

可以看出有移动构造的好处确实很大。不过还有这个编译器还有可能更绝, 在vs2019的release和vs2022的debug和release 。就是他可以跨行优化,这是很牛逼的。什么呢,就是它连两次移动都不移动了。你最终不是都要把资源转移到ret中吗?那我就让str是你的别名,直接实现零拷贝。那这时候又有人问了:这样的话,这个移动语义还有什么意义吗?不是所有的编译器都可以实现跨行优化的。而且,C++标准规定,并没有跨行优化这一说法,所以可以理解为这是编译器的自作主张。 

 3.5.1.2 移动赋值

这个的原理其实跟移动构造差不多,还是上面的那个代码,咱们先来看只有拷贝赋值:

再来看一下既有拷贝赋值,又有移动赋值:

 上面是在Linux系统下看到的没有优化前的,是怎样的。传参是简单的值传递,C++11就有了构造+移动构造。这里形参只是值传递,若想用深层次的传递,可以const string& s 或者左值专属:const string &s ,右值专属:string&&s。其实本质来说,值传递的移动构造不算移动构造,因为移动构造好像只是那些深层次的资源东西。所以说值传递只是所有的值都可以传过去,但是&&只有右值可以,同样耶代表着,你实现的是深层次的移动构造。

ok,咱们现在来讲几个区别?

string && r2 =string("111111");

string ret =string("11111111"); 

那么这两个式子,都接受了这个临时对象,那么有什么区别呢这两个句子?

第一种写法:这里使用的是一个普通的对象(`bit::string r2`)来接收,而不是右值引用。
这是因为我们想要一个具有独立生命周期的对象,而不仅仅是引用一个临时对象。
使用普通对象`r2`来接收函数返回值,这样可以得到一个独立的对象 。

第二种写法:bit::string("111111")`创建了一个临时对象(右值)。`r2`是一个右值引用,
它直接绑定到这个临时对象,从而延长了临时对象的生命周期(使其与`r2`的生命周期相同)。
也就是说,这个临时对象不会在表达式结束时立即销毁,而是会随着`r2`的作用域结束而销毁。
这样也是合法的,并且会将`r2`绑定到函数返回的临时对象上,延长其生命周期。但是,
这样做有一个重要的区别:`r2`是一个引用,而不是一个独立的对象。这意味着:
1. 它不能作为左值进行修改(除非使用`std::move`转换,但通常我们不这样用)。
2. 它只是临时对象的一个别名,没有自己的资源。
所以,你用&&接收的意思就是,类似于取别名,你的临时对象就直接绑定在r2上面了!!!!!

所以说,这两种写法都可以,只不过说,这个&&就像给右值取别名,仅此而已,
而你用一个普通对象去接收的时候,说明你是想修改,用这个对象的。

string addStrings(string num1, string num2)
{
    string str;
    int end1 = num1.size() - 1, end2 = num2.size() - 1;
    int next = 0;
    while (end1 >= 0 || end2 >= 0)
    {
        int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
        int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
        int ret = val1 + val2 + next;
        next = ret / 10;
        ret = ret % 10;
        str += ('0' + ret);
    }

    if (next == 1)
        str += '1';
    reverse(str.begin(), str.end());
    cout << "******************************" << endl;
    cout << &str << endl;
    return str;
}
//bit::string ret = bit::addStrings("11111", "222222222");


string(string&& s)
{
    cout << "string(string&& s) -- 移动构造" << endl;
    this->swap(s);
}
//bit::string&& r2 = bit::string("111111");

OK,观察上面的两个代码,
1.为什么第二个代码形参使用了&&来接收,第一个却没有使用&&?

注意,这里的第二个代码写的意图是为了实现移动拷贝的深层次的拷贝(类似于拷贝构造的深拷贝)
所以才用取别名的方法来接收实参(因为这样可以调用实参里面的资源)
但是第一个代码中的形参不用&&来接收,
这里的形参是值传递(`string`),而不是引用。值传递意味着在调用函数时,
实参会被拷贝(或移动)到形参。
这里传递的是字符串字面值(类型为`const char[N]`),它们会隐式转换为`bit::string`类型。
由于字面值是右值,所以会调用`bit::string`的构造函数(可能是接受`const char* `的构造函数)
来创建临时对象(右值)。然后,因为`addStrings`函数的形参是值传递,所以会用这两个临时对象来
初始化形参`num1`和`num2`。

在初始化形参时,由于临时对象是右值,所以会优先选择移动构造函数(如果可用)来初始化形参,
而不是拷贝构造函数。也就是说,这里会发生移动构造,将临时对象的资源移动给形参`num1`和`num2`。


如果说改用了&&,
那么,函数内部可以直接使用这两个形参,并且知道它们是右值引用(意味着我们可以从中移动资源)。
但是,这种写法会限制函数只能接受右值,而不能接受左值。而值传递的方式既可以接受左值
(触发拷贝构造),也可以接受右值(触发移动构造),并且在函数内部,形参是左值
(因为它们是具名对象)。

在`addStrings`函数中,我们并不需要改变传入的实参(即我们不希望“偷走”实参的资源),
所以使用值传递也是合理的。而且,对于右值实参,移动构造的开销通常比拷贝构造小。


所以说这里其实就是为了传参数而已,这里的移动构造就是给两个形参赋值,仅此而已
这个移动构造,是不是跟拷贝构造一样,也有深拷贝(就是移动资源)
,以及浅拷贝(只是表面的复制数据)。所以说,这个地方的addstring函数的形参,不用&& ,
是因为&& 是右值引用,你可以用,但是用了就相当于是拷贝构造里的深拷贝,
就是将你的资源也给传过去了。而不用&& ,就是说就是表面上简单的传值(复制数据)


形参一般都会用const string& s,是因为这个既可以接收左值的深拷贝,也可以接收右值的深拷贝
而&& 只可以接收右值的深拷贝,当然,你要是不想传资源过去,直接用值传递就可以了,
就是addstring函数那样

用表格来说就是上面的那样。

3.5.2 右值引用和移动语义在容器插入中的提效 

例如上图,C++11更新后,对容器的改变也挺大的,就是引入了一个右值版本的插入。当实参是⼀个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象 。当实参是⼀个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上 。

int main()
{
	//所以,右值以及移动语义就是相当于雪中送炭了。
	 
	 
	
	//右值引⽤和移动语义在容器的增添元素中的提效
	//所有涉及到容器深拷贝的,容器增加元素的,都增加了移动构造。(即&&)
	//这本质也是为了提效率,因为若是string类型,你要是拷贝构造的话,效率不如直接移动构造
	//一般在C++11之前,不管什么值,都是调用的是拷贝构造,这样一些右值,再调用拷贝构造的话
	// 效率不如移动构造。
	//当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上
	std::list <kkk::string> It;
	kkk::string s1 = ("111111111111");//构造
	It.push_back(s1);//拷贝构造

	It.push_back(move(s1));//移动构造
	It.push_back("22222222222222");//构造+移动构造
	
	return 0;
	//这里,s1的本质还是左值,只不过它的属性变成了右值。因为强 转是不可以改变变量的本质特征的。
}

 

这个是有移动构造的版本。可见,右值都是调用的移动构造,这样效率会更高。

这个是没有移动构造的版本,可见不管是右值还是左值,都是调用的拷贝构造版本。

 好,那么咱们再来看一下List的实现:

 template<class T>
    struct ListNode
    {
            ListNode<T>* _next;
            ListNode<T>* _prev;
            T _data;
            ListNode(const T& data = T())
                    :_next(nullptr)
                    ,_prev(nullptr)
                    ,_data(data)
            {}
//我们之前模拟实现的bit::list拷⻉过来,⽀持右值引⽤参数版本的push_back和insert
//但是需要注意的是,这里的支持右值引⽤参数版本的push_back和insert
//但是我们前面说过,就是编译器会将变量表达式识别为左值,所以说,这里的&&x,x会被编译器
识别为左值,所以你要是想调到移动构造,只能在下面所有用到x的地方,全部将其转变为右值
可以使用move()
            ListNode(T&& data)
                    :_next(nullptr)
                    , _prev(nullptr)
                    , _data(move(data))
            {}
    };
    template<class T, class Ref, class Ptr>
    struct ListIterator
    {
            typedef ListNode<T> Node;
            typedef ListIterator<T, Ref, Ptr> Self;
            Node* _node;
            ListIterator(Node* node)
                    :_node(node)
            {}
            Self& operator++()
            {
                    _node = _node->_next;
                    return *this;
            }
            Ref operator*()
            {
                    return _node->_data;
            }
             bool operator!=(const Self& it)
            {
                    return _node != it._node;
            }
    };
    template<class T>
    class list
    {
            typedef ListNode<T> Node;
    public:
            typedef ListIterator<T, T&, T*> iterator;
            typedef ListIterator<T, const T&, const T*> const_iterator;
            iterator begin()
            {
                    return iterator(_head->_next);
            }
            iterator end()
            {
                    return iterator(_head);
            }
            void empty_init()
            {
                    _head = new Node();
                    _head->_next = _head;
                    _head->_prev = _head;
            }
            list()
            {
                    empty_init();
            }
            void push_back(const T& x)
            {
                    insert(end(), x);
            }
            void push_back(T&& x)
            {
                    insert(end(), move(x));
            }
            iterator insert(iterator pos, const T& x)
             {
                    Node* cur = pos._node;
                    Node* newnode = new Node(x);
                    Node* prev = cur->_prev;
                    // prev  newnode  cur
                    prev->_next = newnode;
                    newnode->_prev = prev;
                    newnode->_next = cur;
                    cur->_prev = newnode;
                    return iterator(newnode);
            }
            iterator insert(iterator pos, T&& x)
            {
                    Node* cur = pos._node;
                    Node* newnode = new Node(move(x));
                    Node* prev = cur->_prev;
                    // prev  newnode  cur
                    prev->_next = newnode;
                    newnode->_prev = prev;
                    newnode->_next = cur;
                    cur->_prev = newnode;
                    return iterator(newnode);
            }
    private:
            Node* _head;
    };
 

里面有一些特别注意事项,我在上面的代码中多有标注。

3.6 类型分类

1.C++11以后,进⼀步对类型进行了划分,右值被划分纯右值(purevalue,简称prvalue)和将亡值 (expiring value,简称xvalue)。

2. 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: true 、 nullptr 或者类似 形 a 42 、 str.substr(1, 2) 、 str1 + str2 传值返回函数调用,或者整 、 b , a++ , a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。

3. 将亡值是指返回右值引用的函数的调⽤表达式和转换为右值引用的转换函数的调⽤表达,如 move(x) 、 static_cast(x)

 4.泛左值(generalizedvalue,简称glvalue),泛左值包含将亡值和左值。

3.7 引用折叠

咱们之前已经学过了&与&&,现在大胆一些,有没有一种可能,可以将两个引用折叠起来用?就是比如左引用的右引用。别说,C++还真有这么奇葩的东西。那么搞这个东西有什么用呢?不慌,这个最后说,咱们先来学习一下引用折叠。

C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引用的引用。

 通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规 则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。

typedef int& lref;
typedef int&& rref;

// 由于引用折叠限定,f1实例化以后总是一个左值引用,因为这里T后面的就是&(有左引用的,无论如何都是左引用)
template<class T>
void f1(T& x)
{}

// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用,这个也叫做万能引用因为这里的具体的引用类型要看T的类型
template<class T>
void f2(T&& x)
{}

int main()
{
	// int&& & r = i;
	int n = 0;

	// 引用折叠
	//只要有一个左值引用,则碰撞完之后就都是左值引用
	lref& r1 = n; // r1 的类型是 int&
	lref&& r2 = n; // r2 的类型是 int&
	rref& r3 = n; // r3 的类型是 int&
	rref&& r4 = 1; // r4 的类型是 int&&,只有右值引用折叠右值引用才是右值引用,
	//其余的都是左值引用。

	// 没有折叠->实例化为void f1(int& x)
	f1<int>(n);
	f1<int>(0); // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&>(n);
	f1<int&>(0); // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&&>(n);
	f1<int&&>(0); // 报错

	// 折叠->实例化为void f1(const int& x)
	f1<const int&>(n);
	f1<const int&>(0);

	// 折叠->实例化为void f1(const int& x)
	f1<const int&&>(n);
	f1<const int&&>(0);

	// 没有折叠->实例化为void f2(int&& x)
	f2<int>(n); // 报错
	f2<int>(0);
	// 折叠->实例化为void f2(int& x)
	f2<int&>(n);
	f2<int&>(0); // 报错
	// 折叠->实例化为void f2(int&& x)
	f2<int&&>(n); // 报错
	f2<int&&>(0);

	return 0;
}

具体样例以及注释,还请大家阅读代码。记住一个:但凡有左值引用,碰完之后都是左值引用。只有右值碰右值才是右值引用。

好,有一些关于这个万能引用误导的地方,大家看代码:

//引用折叠 ——> 万能引用,前提是这里得有一个模板
// 这样就可以实现泛型模板了
// 传左值,就实例化做左值引用的函数
// 传右值,就实例化做右引用的函数
// 
// 这里为什么是万能引用呢?
// 这个地方是函数模板,T的类型是通过实参传递推出来的
 
 //	 左值引用
void push_back(const T& x)
{
	insert(end(), x);
}

// 右值引用
void push_back(T&& x)
{
	insert(end(), move(x));
}
/* 所以这个他不是万能引用,因为这里T的类型不是实参传递给形参推出来的,
 是类模板实例化时就确定的
你如果还想再实现万能引用:*/
 // 万能引用,这样就可以了
template<class X>
void push_back(X&& x)
{
	insert(end(),forward<X>(x));
}

就是这个地方的T这个类型之前就已经确定好了,它不像模板一样得现推。

这里出现了一个forward<X>(x),先别管,后面会讲。

Function(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模 板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引⽤版本形参的Function。

下面咱们来看一下万能引用的例子:

template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;
	cout << &a << endl;
	cout << &x << endl << endl;
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	// Function(10); // 右值,证明:由于T为int,所以说a与x的地址不同,因为x不是a的别名

	int a;
	// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
	//Function(a); // 左值,证明:a与x的地址相同

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	//Function(std::move(a));证明:a与x的地址不同

	const int b = 8;
	// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&t)
	// 所以Function内部会编译报错,x不能++
	// Function(b);证明:a与x的地址相同

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	// 所以Function内部会编译报错,x不能++
	Function(std::move(b)); // const左值变为const 右值,证明:a与x的地址不同

	return 0;
}

3.8 完美转发

Function(T&&t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化 以后是右值引用的Function函数。

 但是结合我们在前面的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定 后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传 递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性, 就需要使用完美转发实现。

完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,下⾯示例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给 Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

 //这里完美转发加上万能引用才是真正的一对。他增加了模板的可利用性。实现了泛型
template<class T>
void Function(T&& t)
{
	//那么其实在这里就会有问题,就是咱们知道这种情况下,t会退化为只有左值引用。那么只有左值引用
	//就会有问题呀,你的fun调用的话,你只能打印出左值引用。你就算move(t),那这样的话,只能打印出
	//右值引用。所以说,有一个forward,这个t的类型主要是由尖括号里的T来决定这个t最终是什么类型
	//其实这个forward底层也是使用了强转(static_cast<decltype(arg)&&>(arg))

	// 完美保持他的属性传参
	Fun(forward<T>(t));
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10); // 右值

	int a;
	// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
	Function(a); // 左值

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值

	const int b = 8;
	// a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
	Function(b);

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	Function(std::move(b)); // const 右值

	return 0;
}

所以,在刚才的List中,也得修改一下部分代码:

template<class X>
list_node(X&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(forward<X>(x))
{}
// 万能引用
template<class X>
void push_back(X&& x)
{
	insert(end(), forward<X>(x));
}
template<class X>
void insert(iterator pos, X&& x)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(forward<X>(x));
	// prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	++_size;
}

4 可变参数模板

4.1 基本语法以及原理

C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数目的参数被称 为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函 数参数。 简单通俗来说就是,你这个参数列表想存几个参数就存几个参数。

C++11⽀持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称 为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函 数参数。 

我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板 ⼀样,每个参数实例化时遵循引用折叠规则。

可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

这⾥我们可以使用sizeof...运算符去计算参数包中参数的个数。

例子:

比如通过这个我就可以打印出你这个print函数里面的参数有几个,那么他是怎么知道的呢?

原理:

1.

2.

3.实际上模板会通过2,从而推导出3.但实际上,编译器会直接跳过2,从而直接推导出3.那么咱们来看一下原理的最终运行结果:

其实上面这一个可变模板参数的东西,就可以代替下面的这三个
 本质就是将模板参数泛型化,使其不需要写这么多的模板了 


这下子应该通透了吧。 

template <class ...Args>
void Print(Args... args)
{
	 // 不支持,因为这里参数的类型都不一样,C++11这里还不支持在同一个容器里存不同类型的值
	 // 可变参数模板编译时解析
	 // 下面是运行获取和解析,所以不支持这样用,所以可变模板参数的用法不是这种
	 cout << sizeof...(args) << endl;
	 for (size_t i = 0; i < sizeof...(args); i++)
	 {
		cout << args[i] << " ";
	 }
	 cout << endl;
}

那么这个时候就有同学问了,这个可变模板参数是不是这样用的呀,啊,不是的!下面我们会将如何使用的,不要着急。

4.2 包扩展

对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个 包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元 素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。

 4.2.1 可变参数包的第一个用法

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	// 不能这么写,这是运行时判断逻辑,而咱们要求得是编译时结束条件,编译时应该就是重新写一个函数吧
	if (sizeof...(args) == 0)
		return;
	//sizeof...(args)这个是计算参数的个数的
	cout << x << " ";
	ShowList(args...);
}

void ShowList()//最后还得写一个结束条件
{
	cout << endl;
}

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	ShowList(args...);//编译时递归调用自己,此时已经打印出了一个x,还剩N-1个参数
}

template <class ...Args>
void Print(Args... args)
{
	// N个参数,第一个传给x,剩下N-1参数传给ShowList的第二个参数包
	ShowList(args...);
}

int main()
{
	double x = 2.2;
	Print(); // 包里有0个参数
	Print(1); // 包里有1个参数
	Print(1, string("xxxxx")); // 包里有2个参数
	Print(1.1, string("xxxxx"), x); // 包里有3个参数

	return 0;
}

第一个用法就是编译时递归调用自己。通过函数来打印出参数是什么。

咱们来看一下原理:

使用这一张图来概括原理,足矣。

4.2.2 可变参数包的第二个用法

 还有一种方法可以不用像上面的一样,写这么多的代码就可以打印出参数都是什么。

这个其实直接将参数包依次展开依次作为实参给⼀个函数去处理。

template <class T>
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}

template <class T>
int GetArg(const T& x)
{
	cout << x << " ";
	return 0;
 /*GetArg 必须返回值(组成新参数包)
返回值类型不影响打印操作(仅利用函数调用的副作用)
 因为这个的作用仅仅是为了组成新的参数包(通过cout<<x<<""???),而return是为了让其能够回去,而不至于
 回都回不去。回到Arguments(GetArg(args)...)这个Arguments里面去*/
}

template <class ...Args>
void Arguments(Args... args)//我们只是利用Arguments函数来触发参数包的展开,从而依次调用GetArg。
 //这里我们并不关心这些参数是什么,因为Arguments函数体为空
{}

template <class ...Args>
void Print(Args... args)
{
	// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
	// GetArg的返回值组成实参参数包,传给Arguments
 
	Arguments(GetArg(args)...);//Arguments函数的参数是通过对每个args参数调用GetArg函数并收集返回值形成的参数包。

//Print(1, 2.0, "hello") 会被展开为:
//Arguments(GetArg(1), GetArg(2.0), GetArg("hello"));
// 核心流程::因为从这个地方会先去走GetArg这个函数,从而展开参数包,展开后生成由 GetArg 返回值组成的新参数包
// 然后再由Arguments提供参数包展开的上下文
}

int main()
{
	Print(1, string("xxxxx"), 2.2);

	return 0;
}

原理也在代码中,大家请自行研读。

4.2.3 可变参数包的第三个用法

这个用法咱们先讲emplace系列接口。因为第三个用法跟这个有关。

4.3 emplace系列

template void emplace_back (Args&&... args);  

template iterator emplace (const_iterator position, Args&&... args);

#include"List.h"

int main()
{
	// 效率用法都是一样的
	std::list<int> lt1;
	lt1.push_back(1);
	lt1.emplace_back(2);

	std::list<kkk::string> lt2;
	// 传左值效率用法都是一样的
	kkk::string s1("111111111");
	lt2.push_back(s1);
	lt2.emplace_back(s1);
	cout << "*****************************************" << endl;

	// 传右值效率用法都是一样的
	kkk::string s2("111111111");
	lt2.push_back(move(s2));
	kkk::string s3("111111111");
	lt2.emplace_back(move(s3));
	cout << "*****************************************" << endl;

	// emplace_back的效率略高一筹,对于传模板参数的时候
	lt2.push_back("1111111111111111111111111111");//构造+移动构造+析构
	lt2.emplace_back("11111111111111111111111111");//构造
 //为什么呢?是因为push_back是普通函数,普通函数的value_type在类模板实例化的时候就确定了
 //所以这里确定是左值,就构造+拷贝构造,右值,就是构造+移动构造.但是emplace_back是函数模板,它的参数是通过实参推出来的。看看实参
 //应该是什么类型,(这里实参传的是const char*)那么它直接构造对应的类型,就是这样的一个原理。
	cout << "*****************************************" << endl;

	std::list<pair<kkk::string, int>> lt3;
	//下面演示多参数的
	// 传左值效率用法都是一样的
	pair<kkk::string, int> kv1("xxxxxx", 1);
	lt3.push_back(kv1);
	lt3.emplace_back(kv1);
	cout << "*****************************************" << endl;

	// 传右值效率用法都是一样的
	pair<kkk::string, int> kv2("xxxxxx", 1);
	lt3.push_back(move(kv2));
	pair<kkk::string, int> kv3("xxxxxx", 1);
	lt3.emplace_back(move(kv3));
	cout << "*****************************************" << endl;

	// emplace_back的效率略高一筹
	// push_back 在list实例化时,确定参数类型为pair<bit::string, int>
	// 所以这里传参是一个隐式类型转换,内部在走移动构造到链表节点上
	lt3.push_back({"11111111111111", 1});

	// 不支持,因为emplace_back是函数模板,{ "11111111111111", 1 }不对应任何具体类型
	// 模板无法推演类型,所以直接用最原始的,这个本来就支持多参数的推导,那不如直接用多参数的推导
 
	//lt3.emplace_back({ "11111111111111", 1 }); 
	// 传参数包(里面都是参数的包)后,底层直接将参数包构造到链表节点上,效率高
	//最后直接构造容器上的对象。编译器会直接构造,省去了那些乱起八糟的内容。
	lt3.emplace_back("11111111111111", 1);
	cout << "*****************************************" << endl;
 
	//所以emplace系列直接传容器中构造T对象的参数包(直接传参数),直接构造,效率高
	//所以STL说emplace_back的效率高,但也是分方法的,对于传T对象的参数包的,效率高,但是要是普通的
	//左值,右值,效率跟push_back差不多。(这里的T对象不就是class T)

	return 0;
}

传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下 std::forward(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左 值。

template<class ...Args>//模板类
list_node(Args&& ... args)//类实例化出的对象,存放模板参数(可能一个,可能多个)
	: _next(nullptr)
	, _prev(nullptr)
	, _data(forward<Args>(args)...)  // 不解析参数包的参数,直接用参数包去匹配T类型对象的构造
	//参数包存放的是所有的参数。注意三个不同的类型的写法
	//这里也别忘了使用完美转发,因为编译器会将变量表达式识别为左值
{}
template<class ...Args>
void emplace_back(Args&&... args)
{
	emplace(end(), forward<Args>(args)...);//下面的args...都要改,都要加上forward<Args>
}
template<class ...Args>
void emplace(iterator pos, Args&&... args)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(forward<Args>(args)...);
	// prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	++_size;
}

emplace_back总体而言是更⾼效,推荐以后使用emplace系列替代insert和push系列 。

所以对于传模板参数而言,emplace系列还是比较强悍的。效率很高。

5. 新的类功能:

5.1 默认的移动构造和移动赋值

C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。

这些也就是如果我们没写(析构函数、拷贝构造、拷贝赋值重载,这三个函数是绑定的,因为无资源开毁的时候我们都不写让它浅拷贝就好,如果有的话那么就要深拷贝了,也就是要写了),此时编译器会自己生成移动构造,如果是内置类型就是浅拷贝,自定义类型就会调用它的移动构造,如果无,就浅拷;对于移动赋值也是如此,但是一旦自己提供一个,编译器都不会再自己生成。

因此我们做了一个规定如果无资源申请与销毁我们这几个都不写,让它走默认生成的浅拷贝,反之就都要写;话说得好,移动构造和赋值效率高,说白了只是对那些需要深拷贝的才会提高效率(转移资源)。

5.2 成员变量声明时给缺省值

这里就是以前类和对象讲述的,如果没传实参就会走缺省值,(重要的就是,声明和定义缺省值只能给一个)。

5.3 defult和delete

1.default:C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为⼀些原因 这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了(因为规定了,如果显示的写了拷贝构造或者移动构造,那么另外一个就不会生成了),那么我们可以使用 default关键字显示指定移动构造生成。

2.delete:如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已, 这样只要其他⼈想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语 法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
 {
 public:
     Person(const char* name = "", int age = 0)
         :_name(name)
         , _age(age)
     {}
 Person(const Person& p)
     :_name(p._name)
     ,_age(p._age)
     {}
     Person(Person&& p) = default;
     //Person(const Person& p) = delete;
 private:
     bit::string _name;
     int _age;
 };
 int main()
 {
     Person s1;
     Person s2 = s1;
     Person s3 = std::move(s1);
     return 0;
 }

5.4 final与override

1.final:

我们不希望一个类被继承(或者不让它的子类出现像多态这样的行为)可以在类名后面加上final。

2.override:

只允许加在子类的虚函数后面,检查子类的虚函数是否被重写,如果重写了父类的就不报错,否则直接报错。

这个在咱们的C++多态这一节中有讲解。有需要的博友们可以去阅读一下。

6.STL中的一些变化

1.STL增加了许多新容器,后面这两个主要是常用的:unordered_map和unordered_set。

2. STL中容器的新接⼝函数类型也增加了,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列 接口和移动构造和移动赋值,还有initializer_list版本的构造,以及范围for的遍历等,这些其实会使用就好。

7.lambda

7.1 lambda表达式语法

1.la mbda表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。

lambda表达式语法使用层而言没有类型,所以我们⼀般是用auto或者模板参数定义的对象去接 收lambda对象。

2.lambda表达式的格式:[capture-list] (parameters)-> return type {function boby }。

lambda表达式的组成:

1.[capture-list] :捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引⽤捕捉,也可以是它们俩的混合捕捉,捕捉列表为空也不能省略

 2.(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连 同()⼀起省略。

3.->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此 部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

4.{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使⽤其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略

来看一个简单的lambda表达式:

// 如果lambda体只包含一个return语句,或者返回类型明确,编译器可以自动推导返回类型。
// 否则,可以使用尾置返回类型(->)来指定返回类型。
//	// 匿名函数对象,这个只能传给auto或者模板,让它自己去推
	auto addFunc = [](int a, int b)->int {return a + b;  };//lambda一般定义的是一些小函数
	//auto addFunc = [](int a, int b){return a + b;  };
	cout << addFunc(1, 2) << endl;

7.2 lambda捕捉列表

1.lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。(这也是为什么要进行捕捉的原因)

2.第⼀种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x, y,&z]表示x和y值捕捉,z引用捕捉。

 //lambda本来就是定义在函数内部的,那肯定要用到内部的一些变量,但是你要是一直传参就很烦(例如上面有个例子就是传参)
 //所以就搞了一个捕捉的东西
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
	{
		// 值捕捉的变量不能修改,引用捕捉的变量可以修改
		//a++;并且这里面的是外面的拷贝,就是就算里面的可以修改,那么外面的也修改不了
		b++;
		int ret = a + b;
		return ret;
	};
	cout << func1() << endl;

3.第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表 写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些 变量。

​// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量,所以编译器也不会傻傻的全部将变量全部捕捉来
auto func2 = [=]
{
	int ret = a + b + c;
	return ret;
};
cout << func2() << endl;
	
// 隐式引用捕捉
	// 用了哪些变量就捕捉哪些变量
	auto func3 = [&]
	{
		a++;
		c++;
		d++;
	};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;

​

4.第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其他变量隐式值捕捉, x引用捕捉;[&,x,y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第⼀个元素必须是 &或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必 须是引用捕捉。

	// 混合捕捉
 。当使⽤混合捕捉时,第⼀个元素必须是&或 = ,并且 & 混合捕捉时,
 后⾯的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。
	auto func4 = [&, a]
	{
		//a++;
		b++;
		c++;
		d++;
	};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;

5.lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态 局部变量和全局变量,静态局部变量和全局变量也不需要捕捉用。这也意味着 lambda 表达式中可以直接使 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

int x = 0;
// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
//lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,
// 不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,⽤。
// 这也意味着lambda表达式中可以直接使lambda表达式如果定义在全局位置,捕捉列表必须为空。
//因为没有什么可以捕捉的,自然捕捉列表就是空
auto func1 = []()
	{
		x++;
	};

6.默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改, mutable加在参数列表的后⾯可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以 修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为 空)。 

  默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改
 ,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,
 传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,
 参数列表不可省略(即使参数为空)。
	auto func5 = [&, a]()mutable
	{
		a++;
		b++;
		c++;
		d++;
	};
	func5();
	cout << a << " " << b << " " << c << " " << d << endl;

7.如果你的lambda在类的成员函数内,并且想捕捉类中的成员变量,那么你必须捕捉this指针。

class A
{
public:
	void f()
	{
		int i = 0;

		// 成员函数内部,想用成员变量,捕获this
		//如果说你不使用this就想补获成员变量,基本不可能
		auto add1 = [this, i](int x, int y)->int {return x + y + i + _a1 + _a2; };
		cout << add1(1, 2) << endl;
	}

private:
	int _a1 = 1;
	int _a2 = 2;
};

int main()
{
	A().f();//这个是类中调用成员函数

	return 0;
}

 7.3 lambda的原理

### 原理总结

1. * *生成匿名类 * *:编译器根据lambda表达式生成一个匿名的类(闭包类型)。

2. * *捕获变量 * *:根据捕获列表,在匿名类中生成相应的成员变量(值捕获的变量是拷贝,引用捕获的变量是引用)。

3. * *函数调用运算符 * *:在匿名类中重载operator(),其参数和函数体与lambda表达式一致。如果没有使用mutable,则operator()为const成员函数;否则为非const。

4. * *创建闭包对象 * *:lambda表达式实际上创建了该匿名类的一个实例(闭包对象)。

### 示例分析

考虑以下代码:

```cpp

int main() {

    int a = 10, b = 20;

    auto lambda = [a, &b](int c) mutable -> int {

        a += c;

        b += c;

        return a + b;

        };

    lambda(5);

}

///////////////////////////////////////////////////////

编译器生成的类大致如下:

```cpp

class __lambda_1 {

private:

    int a;  // 值捕获的a

    int& b; // 引用捕获的b

public:

    __lambda_1(int a, int& b) : a(a), b(b) {}

    // 由于使用了mutable,所以operator()为非const

    int operator()(int c) {

        a += c; // 可以修改成员变量a

        b += c; // 修改b引用的对象

        return a + b;

    }

};

int main() {

    int a = 10, b = 20;

    __lambda_1 lambda(a, b); // 创建闭包对象,值捕获a(拷贝),引用捕获b(传递引用)

    lambda(5);

}

///////////////////////////////////////////////
这样,我们就理解了lambda表达式背后的实现机制。需要注意的是,编译器生成的类名是唯一的,
且不可写(所以称为匿名类),我们通常用auto来声明lambda对象。

它的原理可以理解为编译器自动生成一个匿名的类(称为闭包类型),
 并创建该类的对象(称为闭包对象)。这个类重载了函数调用运算符(operator()),
 因此可以像函数一样被调用。
 
 
就像上面的一样,编译器自动生成了__lambda_1这个匿名的类(就是即用即销毁的类),
 这个类别忘了对补获的东西进行构造。
并在main函数里,创建一个闭包对象lambda,这样就可以调用这个匿名的类了。

核心原理:编译器生成闭包类
当编译器遇到 Lambda 表达式时,会自动生成一个匿名的闭包类(closure type),
 并实例化一个该类的对象(闭包对象)。这个类包含以下关键部分:
        1.函数调用运算符 operator()
参数列表和函数体直接来自 Lambda 定义
默认生成 const 成员函数(除非使用 mutable 关键字)
 Lambda: [](int x) { return x * 2; }
class __AnonymousClass {
public:
    int operator()(int x) const {  // 默认 const
        return x * 2;
    }
};
        2.捕获列表 → 成员变量
根据捕获方式生成对应的成员变量:
值捕获:生成与外部变量同名的成员变量(拷贝构造)
引用捕获:生成引用类型的成员变量
 Lambda: [a, &b](){ ... }
class __AnonymousClass {
    int a;     // 值捕获 → 拷贝
    int& b;    // 引用捕获 → 引用
public:
    __AnonymousClass(int a, int& b) : a(a), b(b) {}
    // ... operator() ...
};
        3.构造函数
编译器生成构造函数,用捕获的变量初始化成员变量。

//⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda以后,编译器会⽣成⼀个对应的仿函数的类。
//编译器如何确保每次生成的仿函数都是不同的呢?这个其实是由于每个仿函数内部都有一个UUID,
//这个UUID每次生成的都不一样,所以才会导致仿函数每次生成的都不一样
class Rate
{
public:
	Rate(double rate)
		: _rate(rate)
	{}

	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};

int main()
{
	double rate = 0.49;

 Rate r1(rate);//补获rate用于上面类的构造
	r1(10000, 2);
 
	//从定义的类到这里,为一个lambda
	// lambda
	auto r2 = [rate](double money, int year) {
		return money * rate * year;
	};

	// 函数对象
	//r2(10000, 2);
	//从上一个lambda到这又是一个lambda,故lambda的原理就是上一个lambda。这就是lambda的原理。

	auto func1 = [] {
		cout << "hello world" << endl;
	};
	func1();

	return 0;
}

7.4 lambda的应用

struct Goods
{
	string _name; // 名字
	double _price; // 价格
	int _evaluate; // 评价
	// ...

	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};
//
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };
	// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
	// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
 
 
 //这里,算法库中的sort的第三个参数,可以传仿函数,也可以传lambda
 //但很显然传仿函数的话,可能有点鸡肋,还有,如果这个仿函数名字写成compare1,compare2那你不炸了吗?
	//sort(v.begin(), v.end(), ComparePriceLess());
	//sort(v.begin(), v.end(), ComparePriceGreater());
	// 20:08
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price;
		});//这里不需要写注释,不需要给类起名字,只是一个匿名对象
		//局部函数对象,当前定义当前就用

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate < g2._evaluate;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate > g2._evaluate;
		});//这里比如,你想看价格升序,那么你的断点必须打在降序的sort上面才可以,这样才可以
			//看到升序的sort。


	return 0;
}

8.包装器

包装器分为function以及bind

8.1 function

以上是 function 的原型,他被定义头文件中。

这个看看就行了,很恶心,咱们继续往下看我讲解的就可以了。

这个包装器的作用就是,可以装下储其他的可以调⽤对象,包括函数指针、仿函数,lambda,bing表达式等。

存储的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空。调用空 std::function 的目标导致抛出std::bad_function_call异常。

 这个就类似于秦始皇大一统,统一篆刻,将所有的相同的全部收纳进一个东西里面:

#include<functional>

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

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return (a + b) * 10;
	}
};

class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}

	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
 //这个是调用函数对象(函数指针)
	function<int(int, int)> f1(f);
	function<int(int, int)> f2(Functor());
	auto lf3 = [](int a, int b) {return (a + b) * 20; };
	function<int(int, int)> f3(lf3);

 //写这个代码的意思就是可以有一个vector,里面的类型都是function<int(int, int)>
 //则里面存的都是包装的同一种类型的函数,这样也可以实现打印
	vector<function<int(int, int)>> funcV = { f, Functor(), lf3};
	for (auto f : funcV)
	{
		cout << f(1, 2) << endl;
		//cout << f.operator()(1, 2) << endl;
	}

	// 包装静态成员函数
	//静态成员函数不需要在前面加上&,但是为了统一,还是加上吧
	// 成员函数要指定类域并且前面加&才能获取地址
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(1, 1) << endl;
	//成员函数有一个this指针,所以在包装这个成员函数的时候要小心。
 //可以像下面一样这么写,这个其实也用到了回调。this指针的类型是Plus*,然后下面的使用
 //&ps来匹配这个Plus*,注意,其实也可以写成&Plus(),但是这样写的话,Plus()是一个临时对象
 //对临时对象取地址是很危险的,会访问到野指针
	function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus ps;
	cout << f5(&ps, 1.1, 1.1) << endl;
//也可以像这个下面一样写,这个就是利用f6,调用operator(),进行回调,调用&Plus::plusd
 //使用Plus&&,右值引用去匹配this指针,也可以。
	function<double(Plus&&, double, double)> f6 = &Plus::plusd;
	cout << f6(Plus(), 1.1, 1.1) << endl;
	 
	 
	 
//使用值传递 (Plus 而非 Plus* 或 Plus&&)
//每次调用都会创建对象的完整副本
 
 //底层实现:
 // 编译器生成的等效代码
//double result = Plus::plusd(&temp_copy, 1.1, 1.1);
 
 /*创建 pd 的临时副本 (temp_copy)
将临时副本的地址作为 this 指针传递
调用结束后临时副本被销毁
 
 对象隔离:每次调用使用独立的副本
状态隔离:成员函数的修改不会影响原始对象
安全但昂贵:避免悬空指针但增加拷贝开销*/
 function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
cout << f6(pd, 1.1, 1.1) << endl;

	return 0;
}

8.2 bind

bind 是⼀个函数模板,它也是⼀个可调用对象的包装器,可以把他看做⼀个函数适配器,对接收 的fn可调用对象进行处理后返回⼀个可调用对象。 bind 可以用来调整参数个数和参数顺序。bind 也在这个头文件中。

 调用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/_3....这些占 位符放到placeholders的⼀个命名空间中。

这里咱们只介绍调整参数个数,因为这个用的最多。

​
int Sub(int a, int b)
{
	return (a - b) * 10;
}

int SubX(int a, int b, int c)
{
	return (a - b - c) * 10;
}

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int main()
{
	cout << Sub(1, 2) << endl;

	// bind调整参数顺序
	// 调整参数顺序(不常用)
	// _1始终代表第一个实参
	// _2始终代表第二个实参
	// ...
	auto bSub1 = bind(Sub, _2, _1);
	cout << bSub1(1, 2) << endl;

	// 调整参数个数
	//这个地方就是相当于你给了第二个参数一个缺省值,让第二个参数绑死就是10了,所以你下面的就固定
	//只要传一个就可以了
	auto bSub2 = bind(Sub, _1, 10);
	cout << bSub2(1) << endl;
	//绑死第一个参数,但是第二个还是_1,因为_1始终代表第一个实参,现在这里只有一个实参,所以是_1
	auto bSub3 = bind(Sub, 10, _1);
	cout << bSub3(1) << endl;

	// 分别绑死第123个参数
	auto sub5 = bind(SubX, 100, _1, _2);
	cout << sub5(5, 1) << endl;

	auto sub6 = bind(SubX, _1, 100, _2);
	cout << sub6(5, 1) << endl;

	auto sub7 = bind(SubX, _1, _2, 100);
	cout << sub7(5, 1) << endl;

	// 成员函数对象进行绑死,就不需要每次都传递了
	//当然这里的f6的类型也可以写auto f6
	function<double(Plus&&, double, double)> f6 = &Plus::plusd;
	Plus pd;
	cout << f6(move(pd), 1.1, 1.1) << endl;
	cout << f6(Plus(), 2.2, 1.1) << endl;

	//这个里面参数的个数,就是double(double,double),这个里面参数有两个,那是因为
	// 后面的没绑死的参数就有两个,所以这里才有两个参数
	//auto f7 = bind(&Plus::plusd, Plus(), _1, _2);
	function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
	cout << f7(1.1, 1.1) << endl;
	cout << f7(2.2, 1.1) << endl;
	//但是这里的话,后面绑死了一个参数,就剩一个参数没有绑死,所以说这里就只有一个double参数
	function<double(double)> f8= bind(&Plus::plusd, Plus(),1.1, _1);
	cout << f8(1.1) << endl;
	cout << f8(2.2) << endl;

​

9.结尾语

OK,那么C++11新语法,新特性,本章到此结束,真不容易,三万六千多字。能把这篇文章完整的读下来的博友们,我像你们表达尊敬。 

 那么到了现在,C++算是快更新完了。还有智能指针以及异常。这两章咱们以后讲。OK,本章完.............................

博友们再见!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值