C++11新特性全解析

在这里插入图片描述

前言

之前我们介绍了C++的相关语法,但之前的介绍大多是基于C++98标准的,C++11标准在C++98的标准上进行了扩充,这篇文章就简单介绍一下C++11新扩展的语法。

C++11发展历史

C++11是C++的第二个主要版本,并且是C++98之后的最重要的一次更新。C++11引入了大量新的语法内容,标准化了既有实践,并改进了对C++程序员可用的抽象。C++03与C++11期间花了8年时间,故而这是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新⼀次。如下图所示:
在这里插入图片描述

列表初始化

在C++98中,一般数组和结构体可以用{}进行初始化,如下所示:

#include <iostream>
using namespace std;

struct Point
{
	int _x;
	int _y;
};

int main()
{
	int arr1[] = { 1,2,3,4 };
	int arr2[4] = { 0 };
	Point p = { 1,2 };

	return 0;
}

在C++11之后,一切对象都可以用{}进行初始化,{}方式的初始化也叫做列表初始化。不管是内置类型还是自定义类型,都可以用初始化列表的方式进行初始化,自定义类型的初始化本质是类型转换,在初始化过程中会产生临时对象,但优化以后都变成了直接构造。在{}初始化过程中,也可以省略掉=。
如下所示:

#include <iostream>
using namespace std;

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()
{
	int x1 = { 12 };
	//也可以这样进行初始化
	int x2{ 13 };

	Date d1 = { 2025 };
	//C++98⽀持单参数时类型转换,也可以不⽤{} 
	Date d2 = 2025;


	return 0;
}

除此之外,C++11引入了initializer_list类,之前vector容器中也介绍过,这个类的本质是底层开辟一个数组,将数据进行拷贝。

右值引用和移动语义

在C++98中就有引用的相关语法,而C++11中新增了右值引用的语法特性,这两者本质都是给对象和变量取别名。但二者也有很大的差别。

左值和右值

左值是一个表示数据的表达式(如变量名或解引用指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。定义时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';

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

// 右值:不能取地址 
 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;

左值引用和右值引用

我们来看以下两条语句:

	Type& r1 = x;
	Type&& r2 = y;

其中第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同理,右值引用就是给右值取别名。
通常情况下,左值引用不能直接引用右值,但是const左值引用可以引用右值,右值引用不能直接引用左值,但是右值引用可以引用move(左值),move是库函数里面的一个函数模版,本质是进行强制类型转换。
需要注意的是,变量表达式都是左值属性,也就是说,一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。
二者简单使用如下所示:

#include <iostream>
using namespace std;

int main()
{
	// 左值:可以取地址 
 // 以下的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';
	double x = 1.1, y = 2.2;
	// 左值引⽤给左值取别名 
	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];
	// 右值引⽤给右值取别名 
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");
	// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值 
	const int& rx1 = 10;
	const double& rx2 = x + y;
	const double& rx3 = fmin(x, y);
	const string& rx4 = string("11111");

	// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
	int&& rrx1 = move(b);
	int*&& rrx2 = move(p);
	int&& rrx3 = move(*p);
	string&& rrx4 = move(s);
	string&& rrx5 = (string&&)s;

	// b、r1、rr1都是变量表达式,都是左值 
	cout << &b << endl;
	cout << &r1 << endl;
	cout << &rr1 << endl;
	// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
	int& r6 = r1;
	// int&& rrx6 = rr1;

	int&& rrx6 = move(rr1);
	return 0;
}

当一个临时变量被右值引用绑定后,可以延长临时对象的生命周期,使临时变量的生命周期跟右值引用变量绑定,const的左值引用也能延长临时对象的生存期,但这些对象都无法被修改。如下所示:

int main()
{
 std::string s1 = "Test";
 // std::string&& r1 = s1; // 错误:不能绑定到左值 
 const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期 
 // r2 += "Test"; // 错误:不能通过到 const 的引⽤修改 
 std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期 
 r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改 
 std::cout << r3 << '\n';
 return 0;
}

左值和右值的参数匹配

C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配左值引用的f函数,实参是const左值会匹配const左值引用的f函数,实参是右值会匹配右值引用的f函数。如下所示:

#include <iostream>
using namespace std;

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

void f(const int& x)
{
	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(ci); // 调⽤ f(const int&) 
	f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&) 
	f(std::move(i)); // 调⽤ f(int&&) 
	int&& x = 1;
	f(x); // 调⽤ f(int& x) 
	f(std::move(x)); // 调⽤ f(int&& x) 


	return 0;
}

运行结果如下:
在这里插入图片描述

移动构造和移动赋值

移动构造是一种构造函数,类似于拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是,要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是一个赋值运算符的重载,与拷贝赋值构成函数重载,类似于拷贝赋值函数,但是移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
由于右值引用的对象都是临时对象,移动构造和移动赋值本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值函数那样去拷贝资源,从而提高效率,这也是C++11为什么要引入右值引用的原因。以下是在之前实现过的string类对象中加入了移动构造和移动赋值:

#include<iostream>
#include<assert.h>
#include<string.h>
#include<algorithm>

using namespace std;

namespace cgc
{
	class string
	{
	public:
		//...其它操作
		// 移动构造 
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

		// 移动赋值 
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
		//...其它操作
 };

}

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

	return 0;
}

运行结果如下:
在这里插入图片描述
对于s4来说,相当于直接得到s1对应的对象的数据,同时s1经过move后为临时变量,当移动构造函数结束后,会由系统将s1对应的资源清理掉,这样就大大提升了构造的效率,使得将一个一个数据拷贝构造的过程转换为交换“窃取”资源的过程。C++11中STL库中很多就是通过移动构造的方式来实现相关的库函数。
我们也可以自己实现一个list容器中移动构造方式的插入:

template<class T>
	struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _data;
	//左值引用构造...
	
	
	ListNode(T&& data)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(move(data))
	{}
};
class
{
public:
	//...其它操作
	
	void push_back(T&& x)
	{
		insert(end(), move(x));
	}
	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);
	}
	//...其它操作
};

之前我们介绍过,右值引用的属性是左值,因此,为了要保证右值引用在传递过程的一致性,要确保相应的构造函数也要有右值引用的版本,同时在传递参数过程中要用move函数。

引用折叠

通过模版或typedef中的类型操作可以构成引用的引用时,C++11给出了一个引用折叠的规则:右值引用与右值引用折叠成右值引用,所有其它组合均折叠为左值引用。如下所示:

#include <iostream>
using namespace std;
template<class T>
void f1(T& x)
{

}

template<class T>
void f2(T&& x)
{

}

int main()
{
	typedef int& lref;
	typedef int&& rref;
	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;
}

像f2函数这种函数模板,T&& x参数看起来是右值引用,但由于引用折叠的规则,参数传递左值时就是左值引用,传递右值时就是右值引用,这种函数模板的参数也叫做万能引用。一个简单使用如下:

#include <iostream>
using namespace std;

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); // 右值 
	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;
	// b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
	// 所以Function内部会编译报错,x不能++ 
	Function(b);

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	// 所以Function内部会编译报错,x不能++ 
	Function(std::move(b)); // const 右值 
	return 0;
}

Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
需要注意的是,只有当有右值引用参数的函数未指出模板的具体类型,自动推导才是万能模板,若已经显示指明了参数,那就不是万能模板。

完美转发

上面介绍过,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式是左值,也就意味着Function函数中的t的属性是左值,那么如果将t插入下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。如果我们想要保持t对象的属性,就要使用完美转发。如下所示:

#include <iostream>
using namespace std;

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)
{
	//Fun(t);
	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;
	// b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
	Function(b); // const 左值 
	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)

	Function(std::move(b)); // const 右值 
	return 0;
}

完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的⽅式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引用,forward内部t被强转为左值引用返回。
运行结果如下:
在这里插入图片描述

可变参数模板

基本语法

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

template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}

用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class…或typename…指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后跟…指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通的模板一样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理与模板类似,本质还是去实例化对应类型和个数的多个函数。
同时,可以使用sizeof…运算符去计算参数包中参数的个数。
使用示例如下所示:

#include <iostream>
using namespace std;

template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}

int main()
{
	double x = 2.2;
	Print();
	Print(1);
	Print(1, string("xxxx"));
	Print(1.1, string("xxxx"), x);

	return 0;
}

运行结果如下:
在这里插入图片描述

包扩展

对于一个参数包,我们除了能计算它的参数个数,还可以进行扩展。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。当扩展一个包时,我们还要提供用于每个扩展元素的模式,我们通常在模式的右边放一个省略号(…)来触发扩展操作。如下所示:

#include <iostream>
using namespace std;

void ShowList()
{
	cout << endl;
}

template<class T, class ...Args>
void ShowList(T x,Args&&... args)
{
	cout << x << " ";
	ShowList(args...);
}

template <class ...Args>
void Print(Args&&... args)
{
	ShowList(args...);
}

int main()
{
	Print(1, 4, string("hello"), 1.1);

	return 0;
}

在编译过程中,本质是将可变参数模板通过模式的包扩展,编译器将自动进行递归推导,如下所示:
在这里插入图片描述
同时,也可以通过以下方式进行包扩展:

#include <iostream>
using namespace std;

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

template <class ...Args>
void Arguments(Args... args)
{}

template <class ...Args>
void Print(Args... args)
{
	// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments 
	Arguments(GetArg(args)...);
}

// 本质可以理解为编译器编译时,包的扩展模式 
// 将上⾯的函数模板扩展实例化为下⾯的函数 
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}

int main()
{
	Print(1, string("hello"), 2.2);
	return 0;
}

运行结果如下:
在这里插入图片描述

emplace系列接口

C++11以后STL容器新增了emplace接口,emplace系列的接口均为模板可变参数,功能上兼容push和insert系列函数,但同时对两个函数进行了扩展,emplace还支持直接插入构造对象的参数,这样有些场景会高效一些,可以直接在容器上构造T对象。以下是一个使用示例:

int main()
{
	list<cgc::string> lt;
	// 传左值,跟push_back⼀样,⾛拷⻉构造 
	cgc::string s1("111111111111");
	lt.emplace_back(s1);
	cout << "*********************************" << endl;
	// 右值,跟push_back⼀样,⾛移动构造 
	lt.emplace_back(move(s1));
	cout << "*********************************" << endl;
	// 直接把构造string参数包往下传,直接⽤string参数包构造string 
	// 这⾥达到的效果是push_back做不到的 
	lt.emplace_back("111111111111");
	lt.emplace_back(10, 'x');
	lt.push_back("111111111111");
	cout << "*********************************" << endl;

	return 0;
}

运行结果如下所示:
在这里插入图片描述
emplace简单实现部分如下:

	template<class T>
	struct ListNode
	{
		//...其它操作
		template <class... Args>
		 ListNode(Args&&... args)
		 : _next(nullptr)
		 , _prev(nullptr)
		 , _data(std::forward<Args>(args)...)
		 {}
		//...其它操作
	}
	
	template<class T>
	class list
	{
		//...其它操作
		template <class... Args>
		iterator insert(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(std::forward<Args>(args)...);
			Node* prev = cur->_prev;
			// prev newnode cur
			
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
		
		template <class... Args>
 		void emplace_back(Args&&... args)
 		{
 			insert(end(), std::forward<Args>(args)...);
 		}
 		//...其它操作
	}

新的类功能

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

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

  1. 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的函数中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按自己拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  2. 如果你没有自己实现移动赋值函数重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按自己拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
  3. 如果你提供了移动构造或者移动赋值,编译器不会自动生成拷贝构造和拷贝赋值。

default和delete

在C++中若没有显示生成相关默认的构造函数,可以使用default关键字显示指定默认的构造函数。若不想在类外调用默认的构造函数,只需在该声明加上=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:
	cgc::string _name;
	int _age;
};


int main()
{
	Person s1;
	//Person s2 = s1; //编译报错
	Person s3 = std::move(s1);
	return 0;

}

lambda表达式

表达式语法

lambda表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数的内部,lambda表达式语法使用层来说没有一个具体的类型,所以一般是使用auto或者模板参数定义的对象去接受lambda对象。
lambda的语法格式如下:

[capture-list] (parameters)-> return type { 
	function boby 
}

各部分的组成如下:

[capture-list]:捕捉列表,该列表出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉。
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要传递参数,则可以连同()一起省略。
->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值是此部分可省略。一般返回值类型明确的情况下,也可省略,,由编译器对返回类型进行推导。
{function body}:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

如下所示:

#include <iostream>
using namespace std;

int main()
{
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 6) << endl;

}

捕捉列表

lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
第一种捕捉方式实在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔。如[x, y, &z]表示x和y值捕捉,z引用捕捉。
第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表中写一个=表示隐式值捕捉,在捕捉列表时写一个&表示隐式引用捕捉,这样lambda表达式中国用了哪些变量,编译器就会自动捕捉那些变量。
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其它变量隐式值捕捉,x引用捕捉;[&,x,y]表示其它变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混和捕捉时,后面的捕捉变量必须是值捕捉,同理,=混合捕捉时,后面的变量必须是引用捕捉。
同时,lambda表达式如果在函数局部域中,它可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着,lambda表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉的对象不能被修改,mutable加在参数列表的后面可以取消其常性,传值捕捉的对象就可以修改,但修改的还是形参,不会影响实参。使用该修饰符后,参数列表不可省略(即使为空)。
lambda使用如下所示:

#include <iostream>
using namespace std;

int x = 0;
auto func1 = []()
{
	x++;
};

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
	{
		// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改 
		//a++;
		b++;
		int ret = a + b;
		return ret;
	};

	cout << func1() << endl;
	cout << b << endl;
	// 隐式值捕捉 
 // ⽤了哪些变量就捕捉哪些变量 
	auto func2 = [=]
	{
		int ret = a + b + c;
		return ret;
	};
	cout << func2() << endl;

	// 隐式引⽤捕捉 
 // ⽤了哪些变量就捕捉哪些变量 
	auto func3 = [&]
	{
		a++;
		c++;
		d++;
	};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;
	// 混合捕捉1 
	auto func4 = [&, a, b]
	{
		//a++;

		//b++;

		c++;
		d++;
		return a + b + c + d;
	};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;
	// 混合捕捉1 
	auto func5 = [=, &a, &b]
	{
		a++;
		b++;
		/*c++;
		d++;*/

		return a + b + c + d;
	};
	func5();
	cout << a << " " << b << " " << c << " " << d << endl;
	// 局部的静态和全局变量不能捕捉,也不需要捕捉 
	static int m = 0;
	auto func6 = []
	{
		int ret = x + m;
		return ret;
	};
	// 传值捕捉本质是⼀种拷⻉,并且被const修饰了 
	// mutable相当于去掉const属性,可以修改了 
	// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉ 
	auto func7 = [=]()mutable

	{
		a++;
		b++;
		c++;
		d++;
		return a + b + c + d;
	};
	cout << func7() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;
	return 0;
}

lambda原理

lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会生成⼀个对应的仿函数的类。仿函数的类名是编译按⼀定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
因此,我们在STL设定排序规则的时候,不仅可以定义一个仿函数,也可以传一个lambda表达式,如下所示:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

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(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
	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;
	});
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值