C++基础(二十):常见C++11的新特性

news2024/9/21 14:37:56

           1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C 语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看下C++的历史版本。

C++的发展历史
阶段内容
C with classes类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等
C++1.0添加虚函数概念,函数和运算符重载,引用、常量等
C++2.0更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数
C++3.0进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处 理
C++98C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美 国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)
C++03C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性
C++05C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名 C++0x,即:计划在本世纪第一个10年的某个时间发布
C++11增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库
C++14对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表 达式,auto的返回值类型推导,二进制字面常量等
C++17在C++11上做了一些小幅改进,增加了19个新特性,比如:static_assert()的文 本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等
C++20自C++11以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等
C++23制定ing

         C++还在不断的向后发展。但是:现在公司主流使用还是C++98和C++11,所有不用追求最 新,重点将C++98和C++11掌握好,等工作后,随着对C++理解不断加深,有时间可以去琢磨下更新的特性。 

目录

一、C++11简介

二、统一的列表初始化

2.1 {}初始化

2.2 std::initializer_list

三、类型推导(RTTI)  (这里见入门阶段详细总结)

3.1 auto

3.1.1 auto简介

3.1.2 auto的使用场景

3.2 decltype

3.3 nullptr

四、范围for循环

五、智能指针

六、 STL中一些变化

七、右值引用和移动语义(重点)

7.1 左值引用和右值引用

7.2 左值引用与右值引用比较

7.3 右值引用使用场景和意义

7.4 右值引用引用左值及其一些更深入的使用场景分析

7.5 完美转发

八、lambda表达式

8.1 C++98中的一个例子

8.2 lambda表达式

8.3 lambda表达式语法

8.4 函数对象与lambda表达式

九、线程库

9.1 thread类的简单介绍

9.2 原子性操作库(atomic)

9.3  lock_guard与unique_lock

9.3.1 mutex的种类

9.3.2  lock_guard

9.3.3 unique_lock

9.4 支持两个线程交替打印,一个打印奇数,一个打印偶数

十、新的类功能

10.1 类成员变量初始化

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

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

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

十一、可变参数模板


一、C++11简介

       在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。 从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于 C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言, 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。

二、统一的列表初始化

2.1 {}初始化

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

struct Point
{
    int _x;
    int _y;
};

int main()
{
   int array1[] = { 1, 2, 3, 4, 5 };
   int array2[5] = { 0 };
   Point p = { 1, 2 };
   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); // old style

	// C++11支持的列表初始化,这里会调用构造函数初始化
	Date d2{ 2022, 1, 2 };
	Date d3 = { 2022, 1, 3 };
	return 0;
}

2.2 std::initializer_list

  initializer_list是一个C++11提供的一个轻量级容器,专门用来接收{}内的初始化列表。本质上是一个类模板,由于模板的特性,在构造initializer_list类时,会自动推导{}里的类型,从而完成对自定义类型的构造,  std::initializer_list是什么类型:


#include <iostream>
using namespace std;
int main()
{
	
	auto a = { 10, 20, 30 };
	cout << typeid(a).name() << endl;
	return 0;
}

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加 std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值。自定义类型能支持列表初始化的原因是因为,在其类体中重载了以初始化列表类模板为参数的构造函数。以至于我们能直接使用{}构造一个对象。如下给出一个伪代码样例:

vector(initializer_list<T> l)
     {
         _start = new T[l.size()];
         _finish = _start + l.size();
         _endofstorage = _start + l.size();
         iterator vit = _start;
         typename initializer_list<T>::iterator lit = l.begin();
         while (lit != l.end())
         {
             *vit++ = *lit++;
         }
         //for (auto e : l)
         //   *vit++ = e;
     }

       上面自定义的vector类重载了以initializer_list模板为参数的一个构造函数,有了这个构造函数之后,就能使用{}的方式对自定义类型进行构造。

vector<int> a = { 1,2,3 };
编译器先是根据{}构造出一个initializer_list对象,然后再调用vector类的构造函数

使用示例如下: 

#include <iostream>
#include <vector>
#include <list>
#include <map>
using namespace std;
int main()
{
	vector<int> v = { 1,2,3,4 };
    vector<int> v2 { 1,2,3,4 };

	list<int> lt = { 1,2 };

	// 这里{"sort", "排序"}会先初始化构造一个pair对象
	map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };

	// 使用大括号对容器赋值
	v = { 10, 20, 30 };
	return 0;
}

总结:

       c++11扩大了用{}(初始化列表)的使用范围,使得所有的内置类型和自定义类型都能以一种统一方式进行初始化,包括STL容器等。使用初始化列表时,可以添加等号也可以不添加,类似于构造声明。

三、类型推导(RTTI)  (这里见入门阶段详细总结)

        c++11提供了多种简化声明的方式,尤其是在使用模板时,可以自动的进行类型的推导,在运行时对对象/变量的类型进行识别。

3.1 auto

3.1.1 auto简介

1.是什么?

        auto是一个提示符,提示编译器根据变量的值来推导变量的类型。
        在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

2.为什么?

        对于某些较长或较奇怪的数据类型,可交给编译器自行推导,这样使代码更简洁。

3.怎么用?

  1.  定义变量使用auto可让编译器自动推导变量类型;
  2.  和范围for使用来遍历数组;
  3.  加引用和范围for一起使用来修改数值。

         使用auto定义变量时必须初始化,auto变量无法作为参数。在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

3.1.2 auto的使用场景

1. 使用auto自动识别变量类型

2. auto关键字经常和范围for一起使用,用来遍历数组。

3. auto结合引用和范围for一起使用,用来修改数组。

当容器存储对象较大时,或者这个对象要做深拷贝,如string,最好要给& ,可以减少拷贝,提高效率。

注意:auto不能做形参和返回值! 

3.2 decltype

      decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。

auto varName=value;
decltype(exp) varName=value;

       auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系。auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导,而decltype不要求,因此可以写成如下形式

decltype(exp) varName;

       原则上将,exp只是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误

3.3 nullptr

        见入门阶段的详细介绍。

四、范围for循环

       这个我们在前面已经进行了非常详细的讲解,这里就不进行讲解了,请参考C++入门 +STL容器部分的讲解。

       容器支持范围for的原理:范围for会被编译器替换成迭代器,也就意味着支持迭代器就支持范围for的使用方法。 除了STL容器可以使用范围for用法,数组也可以使用(原生指针可以认为是天然的迭代器),比如:vector/string的迭代器就是原生指针。 

五、智能指针

           这个后面会进行专题讲解。

六、 STL中一些变化

       用橘色圈起来是C++11中的一些几个新容器。

  1. array实际上是定长数组,实际中用的很少,缺点:定长+存储数据的空间在栈上,栈的空间本来就不大,
  2. forward_list实际上是单链表,实际中用的少,缺点:不支持尾插尾删+insert数据也是在当前位置的后面;
  3.  实际最有用的是unordered_map和 unordered_set,推荐使用,因为它们的效率高于map/set

七、右值引用和移动语义(重点)

7.1 左值引用和右值引用

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

什么是左值?什么是左值引用?

       左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。左值引用就是给左值的引用,给左值取别名。

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

什么是右值?什么是右值引用?

       右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值等等临时对象右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

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;      //rr1成为了10的右值引用,具有了名字,他就是左值,可以修改
    const double&& rr2 = x + y;
    rr1 = 20;
    rr2 = 5.5;  // 报错
    return 0;
}

7.2 左值引用与右值引用比较

左值引用总结:

1. 左值引用只能引用左值,不能引用右值。

2. 但是const左值引用既可引用左值,也可引用右值。

int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra1为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;
}

在C++11中,将右值区分成纯右值和将亡值,纯右值指的是基本数据类型 

7.3 右值引用使用场景和意义

        前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引 用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

左值引用的使用场景:做参数和做返回值都可以提高效率。

void func1(string s)
{}
void func2(const string& s)
{}

int main()
{
	string s1("hello world");
	// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率
	func1(s1);
	func2(s1);

	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += '!';
	return 0;
}

左值引用的短板:

        但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回,但是传值返回会(传的是临时对象的拷贝构造)导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。并且是深拷贝,需要开辟内存空间。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using namespace std;
//C++11将右值区分成纯右值和将亡值
//纯右值:常量以及基本数据类型在表达式计算过程中或者函数调用过程中产生的临时对象
//将亡值:自定义类型(对象)的临时对象

class String
{
public:

	String(const char* str = "")
	{
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}


	String(const String&  s)    //左值引用:既可以匹配左值,又可以匹配右值,但是临时对象作为右值(将亡值),进行深拷贝浪费空间!因为临时对象用完就不需要了
	{
		cout << "String(const String&  s)  - 深拷贝" << endl;
		_str = new char[strlen(s._str) + 1];                    
		strcpy(_str, s._str);
	}



	String(const String&& s)   //右值引用:来引用右值(将亡值),我就可以不进行深拷贝,提高效率
		:_str(nullptr)
	{

		cout << "String(const String&& s)  - 移动拷贝" << endl;
		swap(_str, s._str);
	}


	~String()
	{
		delete[] _str;
	}

private:
	  char* _str;

};



String f(const char* str)
{
	String tmp(str);
	return tmp;                         //这里返回的实际上是拷贝tmp的临时对象
}



int main()
{
	String s1("helloworld");
	String s2(s1);                        //参数是左值
	String s3(f("右值-将亡值"));          //参数是右值 -将亡值(传递给你用,用完我就析构了)


	return 0;
}


        右值引用和移动语义解决上述问题: 增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。它就不会调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。当将亡值作为函数参数,既存在拷贝构造,又存在移动构造,编译器会首先去匹配移动构造!

      不仅仅有移动构造,还有移动赋值: 在类中增加移动赋值函数,再去调用。函数中会先生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把识别成了右值,调用了移动构造。然后在把这个临时对象做为函数调用的返回值赋值,这里调用的移动赋值。

STL中的容器都是增加了移动构造和移动赋值。

7.4 右值引用引用左值及其一些更深入的使用场景分析

       按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move 函数将左值转化为右值。C++11中,std::move()函数位于文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

int main()
{
	string s1("hello world");
	// 这里s1是左值,调用的是拷贝构造
	string s2(s1);


	// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
	// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
	// 资源被转移给了s3,s1被置空了。
	string s3(std::move(s1));
	return 0;
}

STL容器插入接口函数也增加了右值引用版本


void push_back(value_type&& val);
int main()
{
	list<string> lt;
	string s1("1111");

	// 这里调用的是拷贝构造
	lt.push_back(s1);

	// 下面调用都是移动构造
	lt.push_back("2222");
	lt.push_back(std::move(s1));
	return 0;
}


运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

7.5 完美转发

      模板中的&& 万能引用

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

         模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。  模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力, 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值, 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。

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

存在的问题:右值引用会在第二次之后的参数传递过程中属性丢失。

如何解决?

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

// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。

template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(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;
}

八、lambda表达式

8.1 C++98中的一个例子

          在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法

#include <algorithm>
#include <functional>
int main()
{
	int array[] = { 4,1,8,5,3,7,0,9,2,6 };
	// 默认按照小于比较,排出来结果是升序
	std::sort(array, array + sizeof(array) / sizeof(array[0]));
	// 如果需要降序,需要改变元素的比较规则
	std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
	return 0;
}

         如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

class 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 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());

	return 0;
}

        随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。

8.2 lambda表达式

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };

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

       上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函 数。

8.3 lambda表达式语法

         其实,他就是一个匿名函数,没有函数名,与普通的函数相比,它没有函数名另外多了一个捕捉列表。返回值可以直接省略不写,由编译器自动推导,没有参数也可以省略。

总结:       

         捕捉列表就是捕捉跟我一个作用域的对象,只有我们进行捕捉了,在lambda函数体内才可以使用,主要分为两种: 传值捕捉和传引用捕捉。传值捕捉的对象是不能在函数体内部进行修改的!如果想要改变,加上mutable就可以进行修改!

  1. 传值捕捉:[a] 捕捉a    [a,b]  捕捉a和b    [=] 捕捉同一作用域的所有对象;
  2. 传引用捕捉:[&a] 捕捉a    [&a,&b]  捕捉a和b    [&] 捕捉同一作用域的所有对象;
int main()
{
    //最简单的lambda表达式, 没有参数,没有返回值就可以不写它们,函数体为空,该lambda表达式没有任何意义
    [] {};

   int a=3,b=4;
   lambda它是定义在函数中的匿名函数

   //实现a+b 的lambda表达式

/*************************传值捕捉:拷贝行为***************************************/
   auto add1 = [](int x1,int x2)->int {return x1 + x2;};   //没有进行捕捉,用的自己函数的形参
   add1(a,b);                                       //和普通函数调用方式一样,需要传入实参
  
   auto add2 = [a,b]()->int {return  a + b;};       //不要参数,进行捕捉,使用捕捉到数据
   add2();                                          //调用

   auto add3 = [=]()->int {return  a + b;};        //不要参数,全部进行捕捉,使用捕捉到数据
   add3();

/*************************传引用捕捉*************************************************/

   auto swap1 = [](int& x1,int& x2)     //没有进行捕捉,用的自己函数的形参
   {
        int x=x1;
        x1=x2;
        x2=x;
   };           
   swap1(a,b);
    

 /* 不正确的用法:传值捕捉实际上是捕捉到的对象的一种拷贝,它无法完成交换!

 auto swap2 = [a,b ]() mutable    
   {
        int x=a;
        a=b;
        b=x;
   };           
   swap2();

*/


   auto swap2 = [&a,&b ]()     //不要参数,进行捕捉,使用捕捉到数据
   {
        int x=a;
        a=b;
        b=x;
   };           
   swap2();


  auto swap3 = [& ]()    //不要参数,全部进行捕捉,使用捕捉到数据
   {
        int x=a;
        a=b;
        b=x;
   };           
   swap3();

/****************************************************************************/   


    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
    [=] {return a + 3; };

    // 省略了返回值类型,无返回值类型
    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;
}

       通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调 用,如果想要直接调用,可借助auto将其赋值给一个变量,变量的类型由编译器自己进行推导。

#include <iostream>
using namespace std;


void (*PF)();
int main()
{
    auto f1 = [] {cout << "hello world" << endl; };
    auto f2 = [] {cout << "hello world" << endl; };
    // 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
   //f1 = f2;   // 编译失败--->提示找不到operator=()
 
 
    // 允许使用一个lambda表达式拷贝构造一个新的副本
    auto f3(f2);
    f3();


    // 可以将lambda表达式赋值给相同类型的函数指针
    PF = f2;
    PF();


    return 0;
}

8.4 函数对象与lambda表达式

       函数对象,又称为仿函数,即:可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。 

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


	// lambda表达式
	auto r2 = [=](double monty, int year)->double {return monty * rate * year;
	};
	r2(10000, 2);
	return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。

       函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

九、线程库

  1. windows操作系统下,有自己的一套API    如:CreateThread()
  2. Linux操作系统下,使用posix的pthread     如: pthread_create

       C++98中,如果你想写多线程的程序,既可以在Windows下跑,也可以在Linux下跑,如何解决?      条件编译

9.1 thread类的简单介绍

        在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接 口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,可以实现跨平台,实现原理:封装库时使用了条件编译,也就是说它的底层还是分别调用了不同平台的线程API,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。

特点:

  1. 跨平台
  2. 面向对象封装的类,每个线程是一个类对象。
函数名功能
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...)构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的参数
get_id()获取线程id
jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程。
jion()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离 的线程变为后台线程,创建的线程的"死活"就与主线程无关

注意:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include <iostream>
#include <thread>

using namespace std;

int main()
{
	thread t1;
	cout << t1.get_id() << endl;
	return 0;
}

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中 包含了一个结构体:

     3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供:

  1. 函数指针
  2. lambda表达式
  3. 函数对象
#include <iostream>
using namespace std;
#include <thread>

void ThreadFunc(int a)
{
    cout << "Thread1" << a << endl;
}

class TF
{
public:
    void operator()()
    {
        cout << "Thread3" << endl;
    }
};



int main()
{
    // 线程函数为函数指针
    thread t1(ThreadFunc, 10);

    // 线程函数为lambda表达式
    thread t2([] {cout << "Thread2" << endl; });

    // 线程函数为函数对象
    TF tf;
    thread t3(tf);

    t1.join();
    t2.join();
    t3.join();
    cout << "Main thread!" << endl;
    return 0;
}

4. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值(右值引用),即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:

  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用jion或者detach结束

9.2 原子性操作库(atomic)

       多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问 题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数 据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。如下代码:

#include <iostream>
#include <thread>
using namespace std;


unsigned long sum = 0;

void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++;
}

int main()
{
	cout << "Before joining,sum = " << sum << endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);

	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << endl;
	return 0;
}

C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。

#include <iostream>
using namespace std;
#include <thread>
#include <mutex>


mutex m;
unsigned long sum = 0;

void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
	{
		m.lock();   //加锁
		sum++;
		m.unlock();  //解锁
	}
}


int main()
{
	cout << "Before joining,sum = " << sum << endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);
	t1.join();
	t2.join();
	cout << "After joining,sum = " << sum << endl;
	return 0;
}

虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻 塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。

因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。

注意:需要使用以上原子操作变量时,必须添加头文件

#include <iostream>
using namespace std;
#include <thread>
#include <atomic>


atomic_long sum{ 0 };


void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++;   // 原子操作
}
int main()
{
	cout << "Before joining, sum = " << sum << std::endl;
	thread t1(fun, 1000000);
	thread t2(fun, 1000000);
	t1.join();
	t2.join();

	cout << "After joining, sum = " << sum << std::endl;
	return 0;
}

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的 访问。 更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型

atmoic<T> t;    // 声明一个类型为T的原子类型变量t

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算 符重载默认删除掉了。

#include <atomic>
int main()
{
     atomic<int> a1(0);
    //atomic<int> a2(a1);   // 编译失败
    atomic<int> a2(0);

   //a2 = a1;               // 编译失败
   return 0;
}

9.3  lock_guard与unique_lock

       在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能 通过锁的方式来进行控制。

比如:一个线程对变量number进行加一100次,另外一个减一100次,每次操作加一或者减一之 后,输出number的结果,要求:number最后的值为1。

#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
using namespace std;


int number = 0;
mutex g_lock;

int ThreadProc1()
{
	for (int i = 0; i < 100; i++)
	{
		g_lock.lock();
		++number;
		cout << "thread 1 :" << number << endl;
		g_lock.unlock();
	}
	return 0;
}


int ThreadProc2()
{
	for (int i = 0; i < 100; i++)
	{
		g_lock.lock();
		--number;
		cout << "thread 2 :" << number << endl;
		g_lock.unlock();
	}
	return 0;
}


int main()
{
	thread t1(ThreadProc1);
	thread t2(ThreadProc2);
	t1.join();
	t2.join();
	cout << "number:" << number << endl;
	system("pause");
	return 0;
}

上述代码的缺陷:锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁 的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。

9.3.1 mutex的种类

在C++11中,Mutex总共包了四个互斥量的种类:

1. std::mutex

C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:

函数名函数功能
lock()上锁:锁住互斥量
unlock()解锁:释放对互斥量的所有权
try_lock()尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻 塞

注意,线程函数调用lock()时,可能会发生以下三种情况:

  1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前, 该线程一直拥有该锁;
  2. 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住;
  3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock);

线程函数调用try_lock()时,可能会发生以下三种情况:

  1. 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量;
  2. 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉;
  3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock);

2. std::recursive_mutex

        其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外, std::recursive_mutex 的特性和 std::mutex 大致相同。

3. std::timed_mutex

        比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。

函数名函数功能
try_lock_for()接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until()接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住, 如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指 定时间内还是没有获得锁),则返回 false。

4. std::recursive_timed_mutex

9.3.2  lock_guard

     std::lock_gurad 是 C++11 中定义的模板类。定义如下:

template<class _Mutex>

class lock_guard
{
public:


	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}


	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}


	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}


	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;


private:
	_Mutex& _MyMutex;
};

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封 装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数 成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁 问题。 lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了 unique_lock。

9.3.3 unique_lock

       与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所 有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动 (move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化 unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解 锁,可以很方便的防止死锁问题。

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

9.4 支持两个线程交替打印,一个打印奇数,一个打印偶数

       本节主要演示了condition_variable的使用,condition_variable熟悉我们linux已经讲过了,他们 用来进行线程之间的互相通知。condition_variable和Linux posix的条件变量并没有什么大的区别,主要还是面向对象实现的。

#include <iostream> // 包含输入输出流库
#include <thread> // 包含线程库
#include <mutex> // 包含互斥锁库
#include <condition_variable> // 包含条件变量库

void two_thread_print()
{
    std::mutex mtx; // 创建互斥锁对象
    std::condition_variable c; // 创建条件变量对象
    int n = 100; // 需要打印的数字范围
    bool flag = true; // 标志变量,用于控制打印奇数或偶数

    // 定义线程t1,用于打印偶数
    std::thread t1([&]() {
        int i = 0; // 初始偶数
        while (i < n) // 循环直到i达到n
        {
            std::unique_lock<std::mutex> lock(mtx); // 获取互斥锁
            // 等待直到flag为true,即t2打印完奇数
            c.wait(lock, [&]()->bool { return flag; });
            std::cout << i << std::endl; // 打印偶数
            flag = false; // 设置flag为false,表示下一个数应该是奇数
            i += 2; // 更新i到下一个偶数
            c.notify_one(); // 通知t2
        }
    });

    // 定义线程t2,用于打印奇数
    std::thread t2([&]() {
        int j = 1; // 初始奇数
        while (j < n) // 循环直到j达到n
        {
            std::unique_lock<std::mutex> lock(mtx); // 获取互斥锁
            // 等待直到flag为false,即t1打印完偶数
            c.wait(lock, [&]()->bool { return !flag; });
            std::cout << j << std::endl; // 打印奇数
            j += 2; // 更新j到下一个奇数
            flag = true; // 设置flag为true,表示下一个数应该是偶数
            c.notify_one(); // 通知t1
        }
    });

    t1.join(); // 等待t1线程结束
    t2.join(); // 等待t2线程结束
}

int main()
{
    two_thread_print(); // 调用two_thread_print函数
    return 0; // 返回0,表示程序正常结束
}

核心思想:

      

十、新的类功能

 默认成员函数

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

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

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载(其实就是前面讲的右值引用作为形参的拷贝构造函数和赋值运算符重载函数,这样做可以提高效率)。针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

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

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& operator=(const Person& p)
    {
       if(this != &p)
       {
          _name = p._name;
          _age = p._age;
       }
       return *this;
    }
	*/


     /*
     ~Person()
      {}
     */


private:
	string _name;
	int _age;
};


int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = move(s1);

	Person s4;
	s4 = move(s2);

	return 0;
}

10.1 类成员变量初始化

        C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这 个我们在类和对象默认就讲了,这里就不再细讲了。

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

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

#include <iostream>
using namespace std;

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;


private:
	string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

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

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

#include <iostream>
using namespace std;

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p) = delete;
private:
	string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;        //这里会报错
	Person s3 = move(s1);  //这里会报错
	return 0;
}

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

  1. 在C++中,override关键字用于明确表示派生类中的成员函数覆盖了基类中的虚函数,帮助编译器进行签名检查以防止拼写错误或参数不匹配;
  2. final关键字用于禁止类被进一步继承或禁止虚函数被进一步覆盖,从而确保设计的安全性和意图的明确性,防止无意中违反设计约定。

十一、可变参数模板

        C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现 阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大 家如果有需要,再可以深入学习。

下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。

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

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数 包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特 点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变 参数,所以我们的用一些奇招来一一获取参数包的值。

递归函数方式展开参数包

#include <iostream>
using namespace std;

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
	cout << t << endl;
}

// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', string("sort"));
	return 0;
}

逗号表达式展开参数包

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

template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', string("sort"));
	return 0;
}

STL容器中的empalce相关接口函数:

template <class... Args>
void emplace_back (Args&&... args);

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和 emplace系列接口的优势到底在哪里呢?

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;
}
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
 std::list< std::pair<int, bit::string> > mylist;
 mylist.emplace_back(10, "sort");
 mylist.emplace_back(make_pair(20, "sort"));
 mylist.push_back(make_pair(30, "sort"));
 mylist.push_back({ 40, "sort"});
 return 0;
}

至此,这一讲内容介绍完毕,内容简单,星光不问赶路人,加油吧,感谢阅读,如果对此专栏感兴趣,点赞加关注!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1926573.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

gd32发送数据,定义参数,接收中断

void usart_receive_data(uint8_t ucch) {usart_data_receive(UART3); } void usart_send_data(uint8_t ucch) {usart_data_transmit(UART3,(uint8_t)ucch);while(usart_flag_get(UART3,USART_FLAG_TBE) RESET); } 这是在c文件中定义函数&#xff0c;之后在h文件中声明&#…

记录些Redis题集(2)

Redis 的多路IO复用 多路I/O复用是一种同时监听多个文件描述符&#xff08;如Socket&#xff09;的状态变化&#xff0c;并能在某个文件描述符就绪时执行相应操作的技术。在Redis中&#xff0c;多路I/O复用技术主要用于处理客户端的连接请求和读写操作&#xff0c;以实现高并发…

eProsima Fast DDS getting started

系列文章目录 文章目录 系列文章目录preface**对象与数据结构**● Publish-Subscriber模块● RTPS模块**配置Attributes** Discovery传输控制前言0、安装cmake安装相关源码安装&#xff1a;- A foonathan_memory_vendor- C fastcdr- D tinyxml2- E asio- F openssl- G fastrtps…

贪心:交换论证法

目录 切蛋糕的最小总开销 切蛋糕的最小总开销 交换论证&#xff1a; 设横切的开销为 h&#xff0c;如果先横切&#xff0c;设需要横切 cnt_h 次。 设竖切的开销为 v&#xff0c;如果先竖切&#xff0c;设需要竖切 cnt_v 次。 先横切&#xff0c;再竖切&#xff0c;那么竖切…

模拟电路再理解系列(3)-共射极放大电路

1.三极管 共射极放大电路的主要器件是三极管&#xff0c;先来梳理一下这个经常见到的元器件 结构 三极管的三个极&#xff1a;基极&#xff0c;集电极&#xff0c;发射极 基极流经的电流来控制集电极和发射极之间的导通和关闭&#xff0c;之前看过一个形象的比喻&#xff0c…

51单片机9(使用左移实现流水灯编程)

一、序言&#xff1a;下面我们来给大家介绍一下这个流水灯&#xff0c;流水灯如何来实现&#xff1f;我们依然使用这个工程来完成它。 1、那要使用实现这个流水灯&#xff0c;那我们只需要让D1到D8逐个的点亮&#xff0c;那同样要实现它足够的点亮&#xff0c;也会涉及到延时&…

windows10 安装Anaconda

文章目录 1. 下载2. 安装3. 配置环境变量4. 检查是否安装成功 1. 下载 官网下载 https://www.anaconda.com/download 下载的最新版本&#xff0c;要求python的版本也高一些 清华大学开源软件镜像站 https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 所有版本在这个网…

在 PostgreSQL 里如何实现数据的分布式事务的回滚和补偿机制?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 在 PostgreSQL 里如何实现数据的分布式事务的回滚和补偿机制一、分布式事务的概念与挑战&#xff08;一…

书生大模型实战营-入门岛-第二关

Python实现wordcount def wordcount(text):words text.split()ans{}for word in words:if word not in ans: ans[word] 1 else: ans[word] 1 return ans text """Hello world! This is an example. Word count is fun. Is it fun to cou…

【日常记录】【CSS】display:inline 的样式截断

文章目录 1. 案例2. css属性&#xff1a;box-decoration-break参考地址 1. 案例 现在有一篇文章&#xff0c;某些句子&#xff0c;是要被标记的&#xff0c;加一些css 让他突出一下 可以看到&#xff0c;在最后&#xff0c;断开了&#xff0c;那如若要让 断开哪里的样式 和 开始…

@google/model-viewer 导入 改纹理 (http-serve)

导入模型 改纹理 效果图 <template><div><h1>鞋模型</h1><model-viewerstyle"width: 300px; height: 300px"id"my-replace-people"src"/imgApi/Astronaut.glb"auto-rotatecamera-controls></model-viewer>&…

【棋盘上的战舰】python刷题记录

目录 小前言 思路&#xff1a; 上代码 lucky ending 小前言 经过漫长的停更周期-----1个月 我决定铁血回归&#xff01;&#xff01;&#xff01; 思路&#xff1a; 两层for循环暴力最快了这种小小范围题&#xff0c;主要是第一行和第一列的边界处理&#xff0c;我分为…

STM32MP135裸机编程:定时器内核时钟频率计算方法

0 工具准备 STM32MP13xx参考手册 1 定时器内核时钟频率计算方法 1.1 定时器分组 STM32MP135的定时器按照时钟源不同分成了三组&#xff0c;如下&#xff1a; APB1: APB2: APB6&#xff1a; 1.2 定时器内核时钟频率计算方法 APB1DIV是APB1的分频系数&#xff0c;APB2DIV、…

docker和docker的安装

1什么是docker&#xff1f; docker是容器技术&#xff08;软件&#xff09;&#xff0c;提供标准的应用镜像&#xff08;包含应用&#xff0c;和应用的依赖&#xff09;可以轻松在docker里安装应用&#xff0c;每个应用独立容器 2.主要功能&#xff1a; 打包&#xff08;软件…

Go:基本变量与数据类型

目录 前言 前期准备 Hello World! 一、基本变量 1.1 声明变量 1.2 初始化变量 1.3 变量声明到初始化的过程 1.4 变量值交换 1.5 匿名变量 1.6 变量的作用域 二、数据类型 1.1 整型 1.2 浮点型 1.3 字符串 1.4 布尔类型 1.5 数据类型判断 1.6 数据类型转换 1.…

STM32 BootLoader 刷新项目 (四) 通信协议

STM32 BootLoader 刷新项目 (四) 通信协议 文章目录 STM32 BootLoader 刷新项目 (四) 通信协议1. 通信流程2. 支持指令3. 通信流程4. 指令结构5. 操作演示 前面几章节&#xff0c;我们已经介绍了BootLoader的整体程序框架&#xff0c;方案设计&#xff0c;以及STM32CubdeMX的配…

51单片机6(P0P1P2P3结构框架图)

一、GPIO结构框架图与工作原理 1、接下来我们介绍一下这个GPIO结构框图和工作原理&#xff0c;我们使用51单片机的GPIO分为了P0&#xff0c;P1&#xff0c;P2&#xff0c;P3这四组端口&#xff0c;下面我们就分别来介绍这四组端口它的一个内部结构&#xff0c;只有了解了内部的…

Python爬虫入门篇学习记录

免责声明 本文的爬虫知识仅用于合法和合理的数据收集&#xff0c;使用者需遵守相关法律法规及目标网站的爬取规则&#xff0c;尊重数据隐私&#xff0c;合理设置访问频率&#xff0c;不得用于非法目的或侵犯他人权益。因使用网络爬虫产生的任何法律纠纷或损失&#xff0c;由使用…

MySQl高级篇-事务、锁机制、MVCC

存储引擎的选择 在选择存储引擎时&#xff0c;应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统&#xff0c;还可以根据实际情况选择多种存储引擎进行组合。 InnoDB&#xff1a;是Mysql的默认存储引擎&#xff0c;支持事务、外键。如果应用对事务的完整性有比较…

C++知识要点总结笔记

文章目录 前言一、c基础1.指针和引用指针和引用的区别 2.数据类型整型 short int long 和 long long无符号类型 3.关键字conststaticconst和static的区别define 和 typedef 的区别define 和 inline 的区别const和define的区别new 和 malloc的区别constexprvolatileextern前置与…