【C++】C++11中的实用语法(一)

今天我们来介绍一下C++11。C++11中增加的语法特性的幅度非常大,我们没有办法也没有必要全要掌握,在这篇博客中我将介绍一些实用的,常用的,重要的语法。

小故事:

1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际
标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫
C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也
完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的
时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。

1. 统一的初始化列表

1.1 {}初始化

在C++98中,标准允许使用大括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

struct Cricle
{
    
    
	int _x;
	int _y;
	int _r;
};
int main()
{
    
    
	int a1[] = {
    
     1, 2, 3, 4, 5 };
	int a2[5] = {
    
     0 };
	Cricle c = {
    
     1, 2 , 3};
	return 0;
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

struct Point
{
    
    
	int _x;
	int _y;
};
int main()
{
    
    
	int x1 = 1;
	int x2{
    
     2 };
	
	int array1[]{
    
     1, 2, 3, 4, 5 };
	int array2[5]{
    
     0 };
	
	Point p{
    
     1, 2 };
	
	// C++11中列表初始化也可以适用于new表达式中
	int* pa = new int[4]{
    
     0 };
	return 0;
}

创建对象时也可以使用列表初始化方式调用构造函数初始化。


class Date
{
    
    
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
    
    
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    
    
	Date d1(2022, 1, 1); //旧的初始化方式
	
	// C++11支持的列表初始化,这里会调用构造函数初始化
	Date d2{
    
     2022, 1, 2 };
	Date d3 = {
    
     2022, 1, 3 };
	Date d4[]{
    
     {
    
    2022,2,2},{
    
    2033,3,3} };
	return 0;
}

1.2 std::initializer_list

std::initializer_list 的介绍文档:
initializer_list

我们在使用stl容器的时候,可能会接触到这样的写法:

	vector<int>v1 = {
    
     1,2,3,4,5 };
	vector<int>v2{
    
     1,2,3,4,5 };

而我们知道,一个自定义类型调用{}初始化,本质是调用对应的构造函数。那么这个构造函数是什么呢?起始就是initalizer_list:
在这里插入图片描述
initializer_list 是什么类型?

int main()
{
    
    
	// the type of lt1 is an initializer_list
	auto lt1 = {
    
     10, 20, 30 };
	cout << typeid(lt1).name() << endl;
	return 0;
}

initalizer_list 支持迭代器,也就是说,我们使用{}初始化容器的时候,initalizer_list 先遍历{}内的数,再将这些数拷贝到容器内。
在这里插入图片描述

比如我们对我们之前模拟实现的vector来增加该功能,我们只需要增加一个新的构造函数:
【C++】手把手教你写出自己的vector类

    
	vector(initializer_list<T>& lt)
	:_start(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
	{
    
    
		typename initializer_list<T>::iterator it = lt.begin();
		while (it != lt.end()) {
    
    
			push_back(*it); // 将{}内的数据push到vector中
			++it;
		}
	}

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。

int main() 
{
    
    
    //这里{"sort", "排序"}会先初始化构造一个pair对象
    map<string, int>dict = {
    
     pair<string,int>("sort",1),pair<string,int>("insert",1) };
	map<string, int>dict2 = {
    
     {
    
    "sort",1},{
    
    "insert",1} };
}

2. 声明

2.1 auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以auto就没什么价值了。

C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

int main()
{
    
    
	//auto int a = 0; 被废弃

	//auto 自动推到对象的类型
	int i = 10;
	auto p = &i;
	cout << typeid(p).name() << endl;//取类型的字符串
}

2.2 decltype

关键字decltype将变量的类型声明为表达式指定的类型。

如果我们不知道某一个变量或者表达式的类型,我们可以通过 typid(p).name 来拿到类型的字符串。但是我们却无法真正取到“类型”,此时我们就需要使用 decltype(推导类型)。

template<class T1,class T2>
void F(T1 t1, T2 t2) {
    
    
    //推导并保存 t1*t2的类型
	decltype(t1 * t2)ret = t1 * t2;
	vector<decltype(t1, t2)>v;
	cout << typeid(ret).name << endl;
}

int main()
{
    
    

	//decltype  推导类型
    //我们不可以 typeid(p) i
	auto pf = strcpy;
	decltype(pf)pf1;
	vector<decltype(pf)>v;
}

2.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针


3. 范围for循环

看过我之前那么多关于stl博客的同学估计也对 范围for 十分熟悉了。
【C++】深入理解String类(一)
范围for 本质上就是 迭代器,这里我们不再赘述了。



5. STL中的一些变化

5.1 新容器

在c++11中增加了 array,forward_list(单链表), unordered_map,unordered_set。

其中实用的只有unordered_map,unordered_set 。 有兴趣的同学可以看这篇博客:
【C++】手把手教你实现自己的unordered_map 和unordered_set

5.1 容器中的一些新方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得
比较少的。

比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是
可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上,其中最有用的新方法是 插入函数的右值引用版本,它提高了stl容器的效率。这在下一个章节
右值引用 和移动语义 中会讲。


6. 右值引用 和 移动语义

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。


6.1 左值 和 左值引用

左值是一个表达数据的表达式,我们可以获取它的地址,对它赋值,可以出现在赋值符号的左边。特殊的,定义时const修饰符后的左值,不能接受赋值,但是依旧可以取地址。

左值的引用就是 给左值的引用,给左值取别名。

int main()
{
    
    
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

6.2 右值 和 右值引用

右值也是一个表示数据的表达式,如:字面变量,表达式返回值,传值返回函数的返回值 等等。右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不可以取地址。

右值引用就是对右值得引用,给右值取别名。

int main()
{
    
    
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

这里需要注意,右值不可以取地址,但是给右值取别名之后,会使右值存储到特定的位置,且可以取到该位置的地址,也就是此时该引用值是一个左值,这也导致了连续的右值引用是错误的。

举个例子:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用

int main()
{
    
    
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	rr2 = 5.5;  // 报错
	return 0;
}

6.3 左值引用 与 右值引用 的比较

  • 左值引用
  1. 左值引用只能引用左值,不能引用右值
  2. 特殊的,const 左值引用 可以引用左值,也可以引用左值
int main()
{
    
    
  // 左值引用只能引用左值,不能引用右值。
  int a = 10;
  int& ra1 = a;  // ra为a的别名
  //int& ra2 = 10;  // 编译失败,因为10是右值
  
  // const左值引用既可引用左值,也可引用右值。
  const int& ra3 = 10;
  const int& ra4 = a;
  
  return 0;
}
  • 右值引用
  1. 右值引用只能引用右值,不能引用左值
  2. 但是右值引用可以引用 move之后的左值
int main()
{
    
    
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	
	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;
	
	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}
  • 参数匹配问题
  1. 如果同时存在 左值引用传参函数 和 右值引用传参函数,那么右值匹配右值的,左值匹配左值的。
  2. 如果只有 左值引用传参函数,那么都匹配左值(因为加了const,所以都可以匹配的)
    void f(const int& a) {
    
    
        //左值引用
	}
	void f(const int&& a) {
    
    
        //右值引用
	}
	int main()
	{
    
    
		int a = 10;
		f(a);
		f(1);
	}

6.4 右值引用的使用场景与意义

介绍了这么多,右值引用相对于左值引用貌似只存在语法上差别,那么c++11到底为何要引入 右值引用呢?

6.4.1 回顾 左值引用

我们先来看看 左值引用的应用场景:

注意:为了获得更好的实现 效果,这一部分我们使用自己模拟实现的string类来做测试。
参考博客:【C++】手把手教你写出你自己的String类

场景一: 左值引用做 函数形参
此时我们使用左值引用可以减少实参传入时的拷贝,提高效率。而使用const 不但可以保护 x的值不被改变,也可以让我们的实参既可以传左值也可以传右值。

    //场景一
	template<class T>
	void push_back (const T& x) {
    
    

	}

我们可以对比一下不使用与使用的情况:
在这里插入图片描述

场景二: 左值引用返回

这里我们将静态变量的别名返回,减少了一次拷贝。但是要注意,如果这个变量只是一个局部变量,那么此时我们采用引用返回时错误的,当函数结束,局部变量或对象被释放,我们取到的别名也是不存在的。

//可以使用左值引用返回,但是效果不明显。
yyk::string&  f2()
{
    
    
	static yyk::string ret;
	return ret;
}
int main()
{
    
    
	yyk::string s1("hello");
	yyk::string ret = f2();
}

我们可以来测试一下(如下图):

我们发现,好像加不加引用不都是一样的吗?并不是
在这里插入图片描述

f1()只调用一次拷贝构造 是由于编译器的优化,实际情况是先拷贝一次产生中间临时变量,再将临时变量拷贝一次给ret1.大部分编译器会对这种“冗余”过程进行优化,直接将 ret拷贝给ret1。优化之后与我们的引用返回效率一致,所以说此时我们引用返回效果不明显。

在这里插入图片描述


6.4.2 右值引用的无效用法

有的同学看到左值有以上缺陷,突发奇想,想出来这样的写法:
在这里插入图片描述其实,这和之前的左值引用返回 是一样的,都是取别名嘛。

6.4.3 右值引用的正确用法

那么右值引用到底该怎么有效使用呢?又用在哪里?

右值引用用于 移动构造 和 移动赋值。

6.4.3.1 移动构造

我们先来看一下 移动构造, 移动构造 与 拷贝构造 不同:

  1. 它只接受 右值,这里的右值通常被叫做“将亡值“,简单来说就是那些即将要结束生命周期的值。
  2. 它不通过深拷贝来构造新对象,而是通过掠夺 右值的资源,占为己有,以此来构造自己。此时构造的消耗远小于 深拷贝,几乎不用任何代价。

移动构造的实现:

        //移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
    
    
			cout << "string(string&& s)-- 移动语义/浅拷贝" << endl;
			swap(s); //交换(掠夺)资源
		}

我们看一个场景来感受一下区别

yyk::string f4()
{
    
    
	yyk::string ret;
	return ret;
}
int main()
{
    
    
	yyk::string s1("hello");
	cout << "f1:" << endl;
	yyk::string ret1 = f4();
}

对于上述代码,我们分别测试 原始版本string 和添加了移动构造后的string的情况:
在这里插入图片描述
那么为什么会出现这种情况呢?对于第一种情况之前已经解释了,我们只解释 添加了移动构造后的情况。

不考虑优化的情况下:添加了移动构造之后,先拷贝构造ret(左值) 产生 中间临时变量 ,此时的中间值就是一个右值,它在下一步拷贝之后就立即释放,所以也称为将亡值。由于是右值,所以匹配 移动构造,ret1掠夺 中间值的资源,构造成功。
在这里插入图片描述
而编译器通常会自动优化,不产生中间临时对象,直接把ret识别为右值(将亡值)。那么就会有两次 移动构造,此时再次优化为一次移动构造。所以我们看到只调用了一次移动构造。
在这里插入图片描述


这时候有同学就要问了,为什么编译器非要先产生一个临时变量呢?这不是自找麻烦吗?

这就是同学们考虑不周了:

我们调用一下自己实现的to_string 函数,如果我们有一个值去接受返回值,确实没必要产生中间临时对象,但是如果我们没有接受值呢?我们直接打印。那么此时我们必须产生一个临时中间对象了,因为返回值处理函数作用域就析构了。

string to_string(int val) {
    
    

		string str;
		while (val) {
    
    
			int i = val % 10;
			str += ('0' + i);
			val /= 10;
		}
		reverse(str.begin(), str.end());
		return str;
}
int main()
{
    
    
	yyk::string ans = yyk::to_string(1234);
	cout << yyk::to_string(1234).c_str(); //没有接受对象,直接输出
}

6.4.3.2 移动赋值

一个深拷贝的类,除了实现拷贝构造和拷贝赋值,还需要实现移动构造与移动赋值。面对这个类,函数传值返回的场景,就可以进一步减少拷贝,提高效率。


string& operator=(string&& s)
{
    
    
	cout << "string& operator=(string&& s) -- 移动语义" << endl;
	swap(s);
	return *this;
}

在这里插入图片描述


6.4.4 右值的分类

  • 内置类型表达式的右值,叫做纯右值,通常不具有实用价值。
  • 自定义类型表达式的右值,叫做将亡值。 比如说:匿名对象,传值返回产生的中间临时对象

6.4.5 move的使用

当我们增加了移动构造,使用移动后的左值s4 构造s3, s4的资源就被转移了。
在这里插入图片描述

6.5 右值引用带来的提升

6.5.1 swap 函数

在c++11之前,我其实是不建议使用swap的,因为swap内部使用了一次拷贝构造和两次拷贝赋值。但是在增加了移动构造,移动赋值之后的C++11中,swap变成了以下的结构,效率得到显著的提升。

在这里插入图片描述

6.5.2 stl容器插入接口

几乎所有的stl容器中的 插入接口 都新增了 右值版本:
在这里插入图片描述
在这里插入图片描述
我们以list为例子测试一下:
在这里插入图片描述

6.6 完美转发

6.6.1 万能引用

模板中的T&& 做参数,不再局限于是右值引用,叫做万能引用。即既可以引用左值,也可以引用右值。

//模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
template<class T>
void F(T&& x) {
    
    }

测试:

template<typename T>
void PerfectForward(T&& t)
{
    
    
	Fun(t);
}
int main()
{
    
    
	PerfectForward(10);      // 右值
	
	int a;
	PerfectForward(a);       // 左值
	PerfectForward(std::move(a)); // 右值
	
	const int b = 8;
	PerfectForward(b);    // const 左值
	PerfectForward(std::move(b)); // const 右值
	
	return 0;

但是,右值在引用之后会”退化“为左值。这就会导致某些问题:如果在我在该函数中再次调用其他函数,此时我们只能匹配到左值引用版本了。

我们可以通过下面的试验验证这一点:
在这里插入图片描述

6.6.2 完美转发

如果我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。

std::forward 完美转发在传参的过程中保留对象原生类型属性

此时我们再测试一下:
在这里插入图片描述


7. 新的类功能

7.1 默认成员函数

原来C++的类中,存在6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值函数
  5. 取地址重载
  6. const 取地址重载

其中 前四个比较重要,后两个用处不大。
在C++11中增加了两个默认成员函数: 移动构造 和 移动赋值重载。

针对 移动构造 和 移动赋值符重载 有一些需要我们注意:

  • 默认移动构造
  1. 默认移动构造的生成条件: 没有实现自己的移动构造函数,且没有实现析构函数,拷贝构造,拷贝赋值重载。
  2. 默认生成的移动构造函数,对于内置类型成员会逐成员按字节拷贝。对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 移动赋值符重载函数
  1. 默认移动赋值重载函数的生成条件:没有实现自己的移动赋值重载函数,且没有实现析构函数,拷贝公祖奥,拷贝赋值重载。
  2. 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果我们提供了移动构造 或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

7.2 类成员变量初始化

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。
特殊的,静态成员可以在类中声明,但是不能在类中初始化,但是 const 静态成员可以(并不是初始化)

class Person
{
    
    
public:
    Person(){
    
    }
private:
	static int _age;
	const static int _npos = -1;

};
int Person::_age = 10;

7.3 强制生成默认函数的关键字default

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

比如说,我们可以强制同时生成移动构造 与 拷贝构造。


7.4 禁止生成默认函数的关键字delete

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


7.5 继承和多态中的final与override关键字


8. 可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

注:但是由于可变模板参数比较哦啊抽象,使用起来技巧比较多,这里我们只点到为止。

在C语言中,我们经常使用的printf,scanf 也都使用了可变参数,Linux中的命令行参数也是可变参数。但是,这些都不是模板可变参数,因为使用模板的话类型是不确定的,实现与使用更加复杂。

下面就是一个基本的可变参数模板:

// Args是一个模板参数包类型,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{
    
    }

int main()
{
    
    
  //可以传任意个数的参数和类型
  ShowList();
  ShowList(1,2,3.33);
  ShowList(1,std::string("hello"));
}

8.1 递归函数展开参数包

我们可以通过下面的代码统计出参数列表中的参数个数:

template<class ...Args>
void ShowList(Args...args)
{
    
    
	for (size_t = 0; i < sizeof...(args); i++) {
    
    
		//cout << args[i] << " ";
		cout << i << endl;
		//这样做无法取出每一个参数
		//通过下标直接取出参数包中的每一个参数

	}
	cout << endl;
}

在这里插入图片描述

如果想取出参数类型,那么我们只能用 递归推演的方式实现:

在这里插入图片描述

8.2 逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式
实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
在这里插入图片描述
另一种可行写法:
在这里插入图片描述

8.3 push_back 和 emplace_back

传左值的时候,没有区别。
传右值的时候,push_back是构造+移动构造,emplace_back是直接构造,区别也不明显

但是,如果push_back/emplace_back的参数对象及其成员没有实现移动构造,那么emplace 依旧是直接构造,push_back则是构造+拷贝构造,此时效率区别很大。

综上所述,稳妥来说,推荐使用emplace_back…

int main()
{
    
    
	std::list< std::pair<int, char> > mylist;
	// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
	// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	mylist.emplace_back(make_pair(30, 'c'));
	mylist.push_back(make_pair(40, 'd'));
	
	mylist.push_back({
    
     50, 'e' });
	for (auto e : mylist)
	cout << e.first << ":" << e.second << endl;
	return 0;
}

9. lambda 表达式

9.1 可调用类型

可调用类型有四种:

  1. 函数指针
  2. 仿函数
  3. lambda函数
  4. 包装器

其中 函数指针 繁琐且可读性差,不好用。仿函数相比函数指针简单的 多,而lambda和包装器是进一步的提升。

  • 仿函数举例:

仿函数的缺点在于不同的规则我们要对应写一个仿函数,且仿函数的命名比较难一眼理解,倘若命名不规范,更是令人恼火。

struct Goods
{
    
    
	string _name;
	double _price;
};
struct Compare
{
    
    
	bool operator()(const Goods& gl, const Goods& gr)
	{
    
    
		return gl._price <= gr._price;
	}
};
int main()
{
    
    
	Goods gds[] = {
    
     {
    
     "苹果", 2.1 }, {
    
     "香蕉", 3 }, {
    
     "橙子", 2.2 }, {
    
    "菠萝",
	1.5} };
	sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
	return 0;
}

所以这个时候我们就可以使用lambda表达式:

  • lambda表达式:
#include<algorithm>

struct Goods {
    
    
	string _name;
	int _price;
	int _evaluate;

	Goods(string name,int price,int evaluate)
		:_name(name)
		,_price(price)
		,_evaluate(evaluate)
	{
    
    }
};
struct ComEvaluteGreater;
int main()
{
    
    
	Goods gds[] = {
    
     {
    
    "苹果 ",2.1,5},{
    
    "香蕉",3,4},{
    
    "橙子",2,3} };
    //lambda表达式
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2) {
    
    
		return g1._price < g2._price;
		});
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2) {
    
    
		return g1._evaluate > g2._evaluate;
		});
}

9.2 lambda表达式的语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement
}

  • lambda表达式各部分说明
  1. [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(不可省略)
  2. (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  5. {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

最简单的lambda

auto f1=[]{
    
    };

实现一个比较整数的lambda,lambda表达式的使用与普通函数基本一致

auto lessFunc = [](int a, int b)->bool {
    
    return a < b; };
cout << lessFunc(1, 2) << endl;

  • 捕获列表说明
    捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
  1. [var]:表示值传递方式捕捉变量var
  2. [=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)
  3. [&var]:表示引用传递捕捉变量var
  4. [&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this)
int main()
{
    
    
  // 最简单的lambda表达式, 该lambda表达式没有任何意义
 []{
    
    };
 
  // 省略参数列表和返回值类型,返回值类型由编译器推导为int
  int a = 3, b = 4;
  [=]{
    
    return a + b; };
 
  // 省略了返回值类型,无返回值类型
  auto fun1 = [&](int c){
    
    b = a + c; };
  fun1(10)
  cout<<a<<" "<<b<<endl;
 
  // 各部分都很完善的lambda函数
  auto fun2 = [=, &b](int c)->int{
    
    return b += a+ c; };
  cout<<fun2(10)<<endl;
 
  // 赋值捕捉x
  int x = 10;
  auto add_x = [x](int a) mutable {
    
     x *= 2; return a + x; };
  cout << add_x(10) << endl;
  return 0;
}

假设我们想实现一个swap:

    int x = 1, y = 2;
	auto swap1 = [](int& a, int& b)
	{
    
    
		int tmp = a;
		a = b;
		b = tmp;
	};
	swap1(x, y);

这样显然是正确的,但是如果说我们要求不传参数实现呢?这个时候我们就必须使用捕捉列表。

这里我们要注意:

  • mutable的作用是让传值捕捉的对象可以修改,但是你修改的是传值拷贝的对象,不影响外面对象,实际中mutale意义不大,除非你就是想传值捕捉过来,只在lambda中修改。

错误写法:

//错误写法
auto swap2 = [x,y]()mutable
	{
    
    
		int tmp = x;
		x = y;
		y = tmp;
	};

正确写法:

auto swap3 = [&]()
	{
    
    
		int tmp = x;
		x = y;
		y = tmp;
	};

在这里插入图片描述

注意:

  1. 父作用域指包含lambda函数的语句块
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a,
    this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,
    否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  3. 在块作用域以外的lambda函数捕捉列表必须为空。
  4. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变
    量都会导致编译报错
  5. lambda表达式之间不能相互赋值,即使看起来类型相同,但是可以互相构造,捕捉,或者赋给相同类型的函数指针

9.3 lambda的本质

lambda并不是真的没有名字,只是我们放弃了冠名权,实际编译器会把他转换为仿函数,这个仿函数的名称为lambda_uuid,uuid是随机生成的,是编译器为了保证每一个lambda生成的类型名称不一样。

与函数对象的对比:

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);
	r1(10000, 2);
	
	// lamber
	auto r2 = [=](double monty, int year)->double{
    
    return monty*rate*year;};
	r2(10000, 2);
	
	return 0;
}

在这里插入图片描述


10. 包装器

包装器是最后一种可调用类型:function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

10.1 为什么需要包装器

举个例子,下面的usef可能是什么呢?可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!

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;
}

在这里插入图片描述
可以看出,对于不同的模板参数,实例出来三个可调用对象,这是低效率,浪费的。我们是否可以对可调用类型的对象进行包装,来减少实例化呢?

10.2 包装器的使用

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;   // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

使用举例:

// 使用方法如下:
#include <functional>
int f(int a, int b)
{
    
    
	return a + b;
}
struct Functor
{
    
    
public:
	int operator() (int a, int b)
	{
    
    
		return a + b;
	}
};
int main()
{
    
    
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;
	
	// 函数对象
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;
	
	// lamber表达式
	std::function<int(int, int)> func3 = [](const int a, const int b)
	{
    
    return a + b; };
	cout << func3(1, 2) << endl;
}

这里有几个需要注意:

  • 对于在类域中的成员函数:
  1. 对于静态成员函数,绑定时,取地址时要指定类域
  2. 对于非静态成员函数,除了取地址时要指定类域,还要把对象传入模板参数表。
class Plus
{
    
    
public:
	static int plusi(int a, int b)
	{
    
    
		return a + b;
	}
	double plusd(double a, double b)
	{
    
    
		return a + b;
	}
};

int main()
{
    
    
   //静态成员函数
   std::function<int(int, int)> func4 = &Plus::plusi;
   cout << func4(1, 2) << endl;
   //非静态成员函数
   std::function<double(Plus, double, double)> func5 = &Plus::plusd;
   cout << func5(Plus(), 1.1, 2.2) << endl;
}

10.3 bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作.

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

可以将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为第二个参数,以此类推。

我们举个例子:

int Plus(int a, int b)
{
    
    
	return a + b;
}
class Sub
{
    
    
public:
	int sub(int a, int b)
	{
    
    
		return a - b;
	}
};

我们可以绑定plus参数分别调用func1的第一,二个参数指定
我们也可以对第二个参数进行强制绑定

std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2);
cout<<func1(1,2)<<endl;//3

std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,10);
cout<<func1(1)<<endl; //11
cout<<func1(1,2)<<endl;//11

此时这种强制绑定在哪里有用呢?我们可以将之前的非静态成员函数进行优化:对于需要绑定的参数,直接绑定值,不需要的参数给 placeholders::_1,placeholders::_2…

//使用绑定进行优化
	std::function<int(int, int) >func4=bind(&Sub::sub,Sub(),placeholders::_1,placeholders::_2);
	cout << func4(1, 3) << endl;

我们还可以调整参数顺序:

std::function<int(int, int) >func5 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << func5(Sub(), 1, 3) << endl;



猜你喜欢

转载自blog.csdn.net/qq_53268869/article/details/124100071