C++11 —— 人中龙凤尚且举步维艰,我等鱼目又怎能一生顺遂

       Hello,大家好,当我们走到这里时,就说明我们关于C++的大部分知识就已经学的差不多了,接下来,我们来学习一下C++11这个补充知识,这部分知识相对来说其实也是比较重要的,因此,我在这里建议大家这部分内容需要我着重掌握。

目录

1 C++11的发展历史

2 列表初始化

  2.1 C++98传统的{ }

  2.2 C++11中的{ }

  2.3 C++11中的std:initializer_list

3 右值引用和移动语义

  3.1 左值和右值

  3.2 左值引用和右值引用

  3.3 引用延长生命周期

  3.4 左值和右值的参数匹配

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

    3.5.1 左值引用主要使用场景回顾

    3.5.2 移动构造和移动赋值

    3.5.3 右值引用和移动语义解决传值返回问题

  3.6 类型分类

  3.7 引用折叠

  3.8 完美转发

4 可变参数模板

  4.1 基本语法原理

  4.2 包扩展

  4.3 emplace系列接口

5 lambda

  5.1 lambda表达式语法

  5.2 捕捉列表

  5.3 lambda的应用

  5.4 lambda的原理

6 STL中的一些变化

7 新的类功能

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

  7.2 声明时给缺省值

  7.3 default和delete

  7.4 final和override

8 包装器

  8.1 function

  8.2 bind

9 const限定符

  9.1 顶层const和底层const

  9.2 constexpr

10 处理类型

  10.1 auto

  10.2 decltype

  10.3 typedef和using


1 C++11的发展历史

       C++11是C++的第二个版本,并且是从C++98起的最重要的一次更新。它引入了大量的更改,标准化了既有实践,并改进了对C++程序员可用的抽象。它最终由ISO在2011年8月12日采纳前,人们曾使用名称"C++0x",因为它曾被期待在2010年之前被发布。C++03与C++11期间总共花了8年的时间,故而这是迄今为止最长的一次版本间隔。从那时起,C++就开始有规律地每3年更新一次。C++11地那次更新可以说是C++历程比较好的一次,它的更新范围设计到了许多的范围,范围之广,影响之大,因此,我们这里需要去着重讲解一下C++11。

2 列表初始化

  2.1 C++98传统的{ }

       我们在c++98中对于一般数组和结构体而言可以用{ }对其进行初始化操作,说白了,其实就是我们在C语言中所学的那个用{ }对一般数组和结构体进行初始化的操作,我们这里就全当再复习一遍。

struct Zyb
{
	int _age;
	const char* _tel;
};
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8 };//用{}对arr数组进行初始化操作。
	struct Zyb zyb = { 19,"zjhy"};//用{}对类型为struct Zyb的变量zyb中的两个成员进行初始化操作。
	return 0;
}

  2.2 C++11中的{ }

       1>.C++11以后想统一它们的初始化方式,试图实现一切对象皆可用{ }来进行初始化操作。

       2>.内置类型支持,自定义类型也支持,自定义类型它的本质其实就是类型转换,中间会产生临时对象,最后编译器在对其进行了优化以后就变成了直接构造(这个知识我们再前面就已经讲过了,大家可以去看看前面的博客)。

       3>.{ }初始化的过程中,C++11允许将 "=" 给省略掉。

       4>.C++11列表初始化的本意是想实现一个大一统的初始化方式,这样会为我们在有些场景下带来不少的福利,如容器push/insert多参数构造的对象时,{ }初始化就会显得很方便。代码分析如下所示:

//我们这里要用{}去统一初始化方式,既然是统一,那我们可以得知其实统一的是内置类型和自定义类型的初始化方式,让它们统一用{}去初始化,既然如此,我们这里还要再另外写一个类去做分析。
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支持的东西,在C++11中依然支持,并不是说更新成C++11之后就不再支持C++98的内容了,其实是支持的,这个我们需要了解一下。
	int x1 = 2;//这是C++98中对int类型的变量X1进行初始化操作。
	int x2 = {2};//C++11中的内置类型也支持{}进行初始化操作。
	int x3{ 2 };//C++11允许我们在使用{}初始化时,将"="省略掉;对于这样的初始化,其实还有另一种理解方法:在我们前面的学习过程中,讲到C++将所有的内置类型全部都设计成了一个类,也就是说(以int类型为例),int类型它也有属于自己的默认构造函数等六大函数,这样说的话,大家对于这样进行初始化的方式应该也就感觉熟悉多了。
	//以上是内置类型的支持,接下来我们来看一下自定义类型的支持方式。
	Date d1 = { 2025,1,1 };//这句代码的本质是用{ 2025,1,1 }去构造一个Date类型的临时对象,然后再调用拷贝函数将临时对象中的数据拷贝到d1中去,但是实际上这里经过编译器的优化后,合二为一变成{ 2025,1,1 }直接构造初始化d1,当我们运行一下,可得知这里确实没调用拷贝构造,这是C++11中的初始化方式。
	Date d2(2025, 1, 1);//这是C++98中的初始化方式,直接将参数给传过去,直接调用构造函数来构造d2。
	Date d3{ 2025,1,1 };//当支持用{}初始化时,C++11允许我们省略掉"="。
	Date d4 = { 2025 };//C++11,当我们进行单参数类型转换时,也支持使用{}。
	Date d5 = 2025;//C++98,注:我们C++98它支持单参数类型转换时,也可以不用加{}。
	//除此之外,这里还有一件需要我们注意的事,就是我们这里写的这个Date类它之所以支持单参数传参,是因为Date类中的构造函数的3个参数都有相对应的缺省值,不需要我们传后两个参数。
	const Date& d6 = { 2024,7,25 };//C++11支持的{}初始化,这里d6它引用的是{ 2024,7,25 }构造的那个临时对象,这句代码编译器它没有做相关的优化,因为没办法做优化,d6是引用,引用的是一个空间,因此编译器它会在这里老老实实地构造一个临时对象,d6引用这个临时对象。
	const Date& d7{ 2024,7,25 };//可以省略掉"="。
	//Date d8 2025;//这句代码是错误的,C++11是不支持这样初始化的,注:只有在使用{}进行初始化时,C++11才允许我们省略"="。
	vector<Date> v;
	v.push_back(d1);//有名对象插入。
	v.push_back(Date(2025, 1, 1));//匿名对象插入。
	//比起有名对象和匿名对象传参插入,我们在这里使用{}会更有性价比。
	v.push_back({ 2025,1,1 });//编译器在这里会用{2025,1,1}构造出一个Date类型的临时对象,再调用赋值构造将产生的那个临时对象中的数据赋值给v中要插入的那个空间中的Date类型的对象。通过这三种插入方式的对比,我们这里可以很明显的感觉到使用{}去进行插入操作比有名和匿名对象传参插入的方式更有性价比。
	map<string, string> dict;
	dict.insert({ "xxx", "yyy" });//用{}去进行传参插入
	return 0;
}

  2.3 C++11中的std:initializer_list

       关于我们接下来要讲解的这个initializer_list,我们前面在讲解list类时我们其实就已经对initializer_list这个类进行过讲解了,这里就再加强巩固一下知识:

       1>.从某种方面来说,上面初始化的方法其实已经和很方便了,但是随着我们对C++的学习逐渐深入并接触到了STL中的一些容器,我们感觉上述的初始化的方法放在对象容器的初始化中是不太方便的,比如说一个vector类类型的对象,我想用N个值去构造初始化,那么为了完成这个目标操作,我们就要实现很多个构造函数才可以支持,比如:vector<int> v1= {1,2,3};(这里我们说的是在C++98中还未支持initializer_list的这种情况下) ,我们这里想用1,2,3这3个值对v1进行初始化操作,若想支持,就必须在vector类模板中再实现一个vector(int x1,int x2,int x3)这样的构造函数才可以;vector<int> v2= {1,2,3,4};若想支持用1,2,3,4这4个值对v2进行初始化,就得另外在vector类模板中另外实现一个vector(int x1,int x2,int x3,int x4)这样的构造函数才可以,综上所述,对于对象容器初始化来说,{ }方式初始化还是有点麻烦。

       2>.鉴于上述所产生的问题,C++11库中提出了一个std:initializer_list的类;auto il={10,20,30};//这个il变量的类型是initializer_list,这个类的本质就是在底层开一个数组,将{}里面的数据全部都拷贝到数组中去,initializer_list这个类的内部其实存有两个指针,这两个指针(指针的类型是initializer_list类中的迭代器类型)分别指向数组的开始和结束。

       3>.std:initializer_list支持迭代器遍历。

       4>.若某个容器它支持一个std:initializer_list的构造函数,也就支持任意多个值构成的{x1,x2,x3...}进行初始化,就是通过std:initializer_list的构造函数支持的,接下来,我们通过代码来细细分析:

//STL中的容器都增加了一个initializer_list的构造,这个我们可以通过C++网址就就可以看到,增加的那个initializer_list是在C++11后支持的,C++98中是没有的,例:
vector(initializer_list<value_type> il,const allocator_type& alloc = allocator_type());
list(initializer_list<value_type> il,const allocator_type& alloc = allocator_type());
//以上是我们展示出来的vector类模板和list类模板中支持initializer_list的那个构造函数。
//说到这里,STL中的容器不止有构造函数支持initializer_list构造了,另外,容器的赋值拷贝也支持initializer_list传参。
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
//OK,接下来,我们就来正式地看一下这里的initializer_list初始化操作。
int main()
{
	vector<int> v1({ 1,2,3,4 });//这里是调用v1对象中的那个参数initializer_list类类型的对象的那个构造函数,"()"代表传参,这里有一个知识需要我们去注意一下,就是只有被"()"括起来的{...},才会去调用容器中相对应的那个参数为initializer_list类类型的对象的构造函数。
	vector<int> v2 = { 1,2,3,4,5 };//这句代码的实现思路和前面的那句代码的实现思路是不同的,对于这句代码而言,编译器在这里会先调用vector类模板中参数为initializer_list类类型的对象的那个构造函数去用{1,2,3,4,5}构造一个临时对象,然后再调用构造函数将刚刚创建的那个临时对象中的数据全部拷贝到v2对象中去。但编译器实际上在这里会对此实现思路进行一个优化,将上面的步骤直接优化成构造v2对象。
	//vector<int>& v3={1,3,5,7,9};//这里编译器会报错,根据前一句代码的解释,我们可知这里v3引用的是临时对象,通过我们前面的学习可知临时对象具有常性,不可修改,而引用(这里指v3)允许修改,这里有权限放大的问题,为了解决这个问题,应该再最前面加上一个const修饰就可以了,这样的话,就是权限平移了。
	const vector<int>& v3 = { 1,3,5,7,9 };
	initializer_list<int> il1 = { 10,20,30,1,1,1 };//这里我们再来简单地看一下initializer_list地底层,它的底层其实就是在栈上开创了一块差不多大地数组空间,将{}里面地数据全部都拷贝到数组中去,它还有两个指针,指针分别指向这个数组的起始和结束位置,这里相应注意的是,数组空间是在栈上开创的。它就类似于C++98中直接定义一块数组,例:
	int il2[] = { 10,20,30,1,1,1 };
	map<string, string> dict({ {"xxx","yyy"},{"sort","zzzz"} });//上面这行代码还会进行一个隐式类型转换的操作,编译器在这里将{"xxx","yyy"},、和{"sort","zzzz"}均转换成pair类类型的对象,然后由于它被一个{}包裹着,因此它会被当成是initializer_list类型,进而去调用map中那个参数为initializer_list类类型的对象的构造函数。
	//OK,C++11过后,编译器会将一个{}的列表直接给给一个对象,那么这个对象的类型就会被识别为initializer_list,这里有一个知识我们注意一下,就是并不是所有的{}列表都会被编译器识别为initializer_list,只有这个类它支持initializer_list类型的初始化时,那么当我们在用{}去进行这个类类型的对象初始化时,此时这个{}列表才会被编译器识别为initializer_list,就比如说我们前面写的Date d3{ 2025,1,1 };这一句代码,{ 2025,1,1 }列表在这里就不会被识别为initializer_list,因为我们没有在Date这个类里面实现参数为initializer_list类类型的对象的构造函数,因此{ 2025,1,1 }在这里并没有被识别为initializer_list;在vector<int> v3({1,3,5,7,9});这句代码中{1,3,5,7,9}会被识别为initializer_list。当然,不止是构造函数,任何一个操作都是如此(拿vector为例),我进行插入操作,当我们插入的参数是一个{}列表,只有当vector中它含有参数为initializer_list类类型的对象的insert成员函数时,{}列表才会被识别为initializer_list,并去调用那个insert函数,如果没有那个insert函数的话,编译器会报错。
	return 0;
}

3 右值引用和移动语义

       C++98的C++语法中就有引用的语法,而C++11中新增了右值引用的语法特性,C++11之后就将我们之前学习的引用就叫做左值引用。无论是左值引用还是右值引用,它的作用都是给对象取别名的。

  3.1 左值和右值

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

       2>.右值也是一个表示数据的表达式,要么是字面值常量,要么是表达式取值过程中创建的那个临时对象等,右值只可以出现在赋值符号的右边,它不可以出现在赋值符号的左边,右值不能取地址。

       3>.值得一提的是,左值的英文缩写为lvalue,右值的英文缩写为rvalue。传统认为它们分别是left value,right value的缩写。现代C++中,lvalue被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,但是不可以取地址,例如:临时变量,字面量常量,存储与寄存器中的变量等等,总的来说左值和右值的核心区别就是能否取地址。

int main()
{
	//左值,可以取地址,既可以在赋值符号的左值,也可以在赋值符号的右边。
	int* p = new int(0);
	int b = 1;
	const int c = b;
	int a = b;
	*p = 20;
	string s("111111");
	s[5] = 'x';
	//以上的几行代码中:p、b、c、a、*p、s、s[5]这些都是比较常见的左值,这些左值都是可以取到地址的。
	cout << &c << endl;//输出一串字符,那串字符其实就是c变量的地址。
	cout << (void*)&s[5] << endl;//我们在输出s[5]这个元素的地址时,我们这里需要将它的类型转换为void*类型,s[5]这个元素是char类型的,既然如此,那么这个元素的地址的类型就是char*类型,那么编译器它在输出这个char*类型的地址时,它会按照char类型的输出规则取输出s[5]这个元素的地址(通过我们前面的·1学习可知,char类型的输出是有许多规则的,比如说遇到'\0'就停止等等一些规则),在输出时难免会产生一些问题,因此将这个地址的类型char*转换成void*类型之后,就可以将其完全输出了。
	//右值,不可以取地址,右值只能出现在赋值符号的右边。
	10;
	double x = 1.1, y = 2.2;
	x + y;
	fmin(x, y);//fmin是C++库中的一个函数,它的作用是返回x和y这两个值中最小的那个值,返回的那个值是临时变量。
	string("111111");//构造一个匿名对象。
	//以上的几行代码中,它们这些值均为右值,只有数据值,不能取地址。
	//cout<<&10<<endl;//编译器会报错,不可以对右值取地址。
	return 0;
}

  3.2 左值引用和右值引用

      1>.Type& r1=x;Type&& rr1=y;在这两个语句中,第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。

       2>.左值引用不能直接引用右值,但是const左值引用可以引用右值。

       3>.右值引用不能直接引用左值,但是右值引用可以引用move(左值)。

       4>.move其实是库里面的一个函数模板,本质上内部是进行强制类型转换,当然我们这里先简单了解一下,因为它还涉及到一些引用折叠的知识,后面会细讲。

template<class T>

typename remove_reference<T>::type&& move(T&& arg) noexcept;

       5>.需要注意的是变量表达式它都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量或表达式的属性是左值(变量本身其实也可以看作是一个表达式)。

       6>.从语法层面来看的话,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度去看r1和rr1的汇编层实现,底层都是用指针实现的,没有什么区别。底层汇编等实现和上层语法所表达的意义有时是背离的,所以不要将其混到一起取理解,互相佐证,只有做不仅不好理解,反而会然我们陷入迷途。

int main()
{
	int* p = new int(0);
	int b = 1;
	const int c = b;
	int a = b;
	*p = 20;
	string s("111111");
	s[5] = 'x';
	double x = 1.1, y = 2.2;
	//这里我们使用左值引用和右值引用分别引用3.1中缩写的那些左值和右值。
	int*& p1 = p;
	int& b1 = b;
	const int& c1 = c;
	int& a1 = a;
	int& p2 = *p;
	string& s1 = s;
	char& s2 = s[5];
	//以上几行代码是我们写的左值引用取引用左值。
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("111111");
	//以上的几行代码是我们这里写的右值引用去引用右值。
	//左值引用不能直接引用右值,但是const左值引用可以引用右值。
	const int& r1 = 10;
	const double& r2 = x + y;
	const string& r3 = string("111111");
	//右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
	int&& r4 = move(b1);
	const int&& r5 = move(c1);
	string&& r6 = move(s1);
	char&& r7 = move(s2);
	//我们上述所写的代码中的b,b1,rr1其实都是变量表达式,它们的属性都是左值属性,因此都可以取地址。
	cout << &b << endl;
	cout << &b1 << endl;
	cout << &rr1 << endl;
	//这里需要注意的是,rr1这个变量的属性是左值,因此它要被右值引用绑定的话,就不能直接绑定了,而是要move一下才可以。
	int&& rr5 = move(rr1);
	//int&& rr6=rr1;//编译器在这里会报错,右值引用不能直接引用左值。
	int& rr7 = rr1;
	return 0;
}

  3.3 引用延长生命周期

       右值引用可用于为临时对象/匿名对象延长生命周期,const的左值引用也能延长生命周期/匿名对象的生命周期,虽然是将它们的生命周期延长了,但这些对象依旧无法被修改。

int main()
{
	string s1 = "Test";
	const string& r1 = s1 + s1;//"s1 + s1"这个表达式调用的是s1对象中的那个operator+()成员函数,它返回了一个string类类型的临时对象,r1它引用的就是operator+()这个成员函数所返回的那个临时对象,引用了临时对象之后,那么这个临时对象的生命周期就被延长了,换句话说,就是这个临时对象的生命周期和r1这个变量的生命周期绑定在一起了,r1不销毁的话,那个临时对象的生命周期还未到,反之r1销毁的话,就说明那个临时对象的生命周期到了。
	//r1+='T';//编译器会报错,r1被const修饰,无法修改。
	string&& r2 = s1 + s1;//r2右值引用的是"s1 + s1"这个表达式所返回的那个临时对象,使得那个临时对象的生命周期与r2这个变量的生命周期绑定在一起了。
	r2 += 'T';//通过我们前面的学习可知,右值引用变量或表达式的属性是左值,那就说明r2这个变量是可以修改的(如果在上一句代码的最前面加上一个const修饰的话,那么r2自然就无法被修改)。
	return 0;
}

  3.4 左值和右值的参数匹配

       1>.C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配到,但是不能修改。

       2>.C++11以后,假如说我们同时重载了左值引用,const左值引用,右值引用作为形参的f函数,那么实参如果是做值的话会匹配f(左值引用),实参如果是const左值的话会匹配f(const左值引用),实参如果是右值的话则会匹配f(右值引用)。

       3>.右值引用变量在用于表达式时,它的属性是为左值,这个设计在这里会让人感觉到很怪,下一小节我们讲右值引用的使用场景时,就能体会到这样设计的价值了。

void f(int& x)//左值引用变量为形参的f函数。
{
	cout << "左值引用重载f(" << x << ")" << endl;
}
void f(const int& x)//const左值引用变量为形参的f函数。
{
	cout << "const左值引用重载f(" << x << ")" << endl;
}
void f(int&& x)//右值引用变量为形参的f函数。
{
	cout << "右值引用重载f(" << x << ")" << endl;
}
int main()
{
	int i = 1;
	const int ci = 2;
	f(i);//左值引用重载f(1);这里调用的是void f(int& x)这个函数。
	f(ci);//const左值引用重载f(2);这里调用的是void f(const int& x)这个函数。
	f(3);//右值引用重载f(3);这里调用的是void f(int&& x)这个函数。对于这句代码我们再来着重地讲解一下,通过我们前面的讲解可知编译器在这里会将3这个数据识别为右值,既然实参是右值,那么调用的自然就是参数为右值引用变量的f函数;那么如果这里没有那个参数为右值引用变量的f函数呢?OK,通过我们前面所讲过的知识可知,const左值引用可以引用右值,根据这个知识,我们可以明确的得知这里调用的是void f(const int& x)这个函数。
	int&& x = 1;//x是一个右值引用变量。
	f(x);//左值引用重载f(1);这里调用的是void f(int& x)这个函数,为什么是调用void f(int& x)这个函数呢?我们前面讲过右值引用变量的属性是左值,也就相当于这句代码是传了一个左值给f函数。
	f(move(i));//右值引用重载f(1);在前面的代码中我们得知i是左值,move(i)之后,那么参数就相当于是一个右值,因此这句代码调用的是void f(int&& x)这个f函数。
	f(move(x));//右值引用重载f(1);通过我们所学过的知识可知,x的属性是左值,将其move之后,就相当于实参变成了一个右值,因此这句代码调用的是void f(int&& x)这个f函数。
	return 0;
}

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

    3.5.1 左值引用主要使用场景回顾

string addStrings(string num1, string num2)
{
	string str;
	//对str这个对象执行一系列操作。
	return str;
}

       左值引用主要的使用场景是在韩式中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决了绝大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如上述所写的addStrings这个函数,C++98中的解决方案只能是被迫使用输出型参数(也就是在main函数中提前创建一个返回值类型的变量,把这个变量作为参数传过去,那么,函数中所有的操作均在这个变量上进行,就可以减少拷贝了)解决。那么C++11以后这里可以使用右值引用返回值解决吗?显然是不可以的,因为这里的本质是返回一个局部对象,函数结束以后这个对象就被销毁了,右值引用返回值也无法改变函数结束后局部对象被析构销毁的事实。

    3.5.2 移动构造和移动赋值

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

       2>.移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似于拷贝赋值重载,移动赋值函数要求第一个参数必须是该类类型的右值引用。

       3>.对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要"窃取"引用的右值对象的资源,而不是像拷贝赋值那样取拷贝资源,从而提高效率。我们通过下面的代码来看一看,注:这里一定要结合创建分析理解。

class string
{
public:
	string(const char* str = "")//构造函数
		:_size(strlen(str))
		,_capacity(_size)
	{
		_str = new char[_capacity + 1];
		strcpy(_str,str)
	}
	string(const string& s)//拷贝构造函数
		:_str(nullptr)
	{
		reserve(s._capacity);
		cout << "拷贝构造" << endl;
		for (auto ch : s)
		{
			push_back(ch);
		}
	}
	string(string&& s)//移动构造函数
	{
		cout << "移动构造" << endl;
		swap(s);//其实就是掠夺右值中的资源,可以通过交换达到掠夺的目的。
	}
	string& operator=(const string& s)//赋值拷贝函数
	{
		cout << "赋值拷贝函数" << endl;
		//实现一系列基本操作,这里由于时间关系,因此就不写了。
		//...
		return *this;
	}
	string& operator=(string&& s)//移动赋值函数
	{
		cout << "移动赋值函数" << endl;
		swap(s);
		return *this;
	}
	//......(其他的一些基本函数,由于某种原因,这里暂时就先不写了。)
	//OK,我们前面分别写了移动构造和移动赋值等一些成员函数,这两个函数本质上就是为右值而准备的,也就是说,它放在这里主要就是为临时对象和匿名对象而设计的,我们可以看到这里所设计的那个移动构造和移动赋值这两个函数,我们的底层是去掠夺那个右值中的值(换句话说,其实就是交换),接下来,我们来看3.5.1中所写的那个addStrings这个函数(我们这里假设编译器不进行优化),按照我们之前所学过的知识可知,这里会构建出一个string类型的临时对象出来,构造出临时对象之后,就会调用拷贝构造(假设这里暂时还没有移动构造这个函数)将临时对象中的所有数据全部拷贝一份给给接收addStrings这个函数的返回值的那个string类型的对象。这样的话,有点麻烦,倘若我们这里再加上移动构造函数的话,那么上述操作的后半部分就不会去调用构造函数了,而是调用移动构造,临时对象是一个右值,因此它必然在这里ihui选择移动构造这个函数,不仅是这样,移动构造函数的内部是进行交换操作,会大大提升运行效率。
private:
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;
};
int main()//我们用上述所写的string类来说明,里面所有的成员函数均可使用
{
	string s1("111111");
	string s2("222222");
	string s = addStrings(s1, s2);//addStrings这个函数它最后生成了一个临时对象,这里是调用移动构造函数将临时对象中的所有数据全部掠夺给了s对象。
	string st;//调用构造函数生成了一个string类型的对象st。
	st = addStrings(s1, s2);//这里是调用移动构造将临时对象中的所有数据全部掠夺给了st对象。
	return 0;
}

      这里的移动构造是针对于右值发明出来的,大家不妨想一想,就是我们这里的传值返回时,中间往往会生成一个临时对象,这个临时对象的生命周期也就仅仅在当前这一行,也活不了多长,他的作用也就是一个临时保管数据,总的来说,就i是作用不大,命短,事还多(里面的数据复制来复制去太麻烦了),为了更加方便高效,就i直接在它临死前将它其中的数据给抢夺了,这样使得效率更加高效方便了,这样就不用再将临时对象中的数据再复制一遍了,大大提高了效率。

    3.5.3 右值引用和移动语义解决传值返回问题

       这个问题我们在上面刚刚讲解的3.5.2中已经大致地讲解过了,这里我们就再看一遍,增强巩固一下。(我们这里先写一段代码,我们接下来是通过这段代码来解释这个返回值传值的问题)

string addStrings(string num1, string num2)
{
	string str;
	//......;对str对象进行一系列操作,这里不是重点,因此就不写了。
	return str;
}
//场景1
int main()
{
	//我们这里要展现的是移动构造和移动赋值这两种情况,因此我们这里通过两个情景来展开。
	string ret = addStrings("111111", "222222");//string就是我们前面所实现的那个代码,这一步会调用拷贝构造。
	return 0;
}
//场景2
int main()
{
	string ret;
	ret = addStrings("111111", "222222");//这一步必须调用赋值拷贝函数。
	return 0;
}

       下面我们就开始正式讲解了,我们这里的正式讲解的过程是通过3幅图 来呈现的,我们先来简单地说明一下这三幅图的一个基本情况(每种情况下都有3幅图,不是整个的讲解过程只有3幅图)。

       1>.第一幅图展示的是不进行优化的情况。一步紧挨着一步走。

       2>.第二幅图为编译器进行初级优化的情况,节省了一些不必要的拷贝。

       3>.第三幅图则是编译器的终极优化,优化非常恐怖。

                                                               场景1

       1>.右值对象构造,只有拷贝构造,没有移动构造的情况: 

        2>.右值对象构造,有拷贝构造,也有移动构造的情况:

       好了,以上的6幅图就是我们这里 所画的优化,我们先看1>.这种情况,由原来的进行2次拷贝构造到后面的进行1次拷贝构造,再到最后的合三为一,不用走拷贝构造,优化在一步一步地变好,不断地去减少拷贝的次数,因为拷贝的代价实在是有点大,因此我们要进行优化,减少拷贝,提高效率;在C++11后编译器这里引入了移动构造之后,这里就会减少拷贝,因为我们前面已经大致了解了拷贝构造,它的机制基本就是"抢夺",不会有开空间这样的拷贝,这样的话,编译器运行的效率会大大提高,由此便可知,不管是不优化、1代优化、2代优化这3种优化中的哪一种优化,调用移动构造的效率永远要比调用拷贝构造的效率好很多,因为减少了拷贝,说到这里,想必有的同学就会在这里问了,通过我们上面的六幅图可得知,不管是1>.还是2>.这两种情况中的哪一种情况,它优化到最后(也就是2代优化),编译器在这里甚至都不调用拷贝构造和移动构造了,那么此时这里所讲的这个移动构造就没有用了呀!对于这个问题,大家不妨想一想,我们将来所工作的公司所用的编译器偏老,它没有这些优化的作用该如何呢?如果没有2代优化这个优化的操作,那么移动构造用起来就比拷贝构造香太多了,效率快多了。总之,不管如何,移动构造它相对于拷贝构造来说就好的太多了。

                                                                场景2

       3>.右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的情况:

       4>.右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的情况:

 

       我们这里的这个解释和前面的那个解释基本相同,不管是在不优化、1代优化、2代优化这3种优化情况的哪一种情况,移动构造和移动赋值比拷贝构造和赋值构造要好很多,使用移动构造和移动赋值会让编译器的效率提升不少。

       通过我们前面的所学知识,我们现在来总的分析一下这个左值引用的优缺点,及对于左值引用的不足的解决方案。

       左值引用优点: 

       1>.左值引用和右值引用它们最终的目的其实就是为了减少拷贝,从而提高程序的运行效率。

       2>.左值引用还可以修改参数/返回值,方便去使用。

       左值引用不足:

       1>.部分函数的返回场景,有的函数的返回值只能传值返回,不能使用左值引用返回。

       2>.当前函数的局部对象,出了当前的这个函数作用域后就代表这个局部对象的生命周期到了,就销毁了,不能用左值引用返回,只能传值返回。

       解决发案:

       1>.不用返回值,这里直接用输出型参数解决,这样做的话会牺牲代码的可读性。

       2>.编译器在这里会进行优化,就如我们前面所画的那12幅图种所示的那样。(注:这个优化并不是C++标准规定的,不同的编译器的优化方式可能会有所不同)

       3>.根据C++11的新标准语法来进行处理(这里指的是右值引用和移动语义,我们前面刚刚讲解的那几幅图其实就是编译器进行优化处理的过程)。

       我们通过上述所画的12幅图可知,不管是对于哪种情况而言,我们在进行传值返回的操作时,移动语义的运行效率往往要比拷贝/赋值的效率高很多,如果我们大家去打开C++官网去看各个类模板的构造函数的话,我们会发现vector/string/map...类模板已经实现了移动语义(在C++11情景下),但是诸如pair<int,int>...等类模板,它们并未实现移动语义,为何呢?其实并不是每个类都需要在这里实现移动语义的,如果我们大家在这里进行仔细对比去看的话,我们会发现移动语义他只会出现在底层会进行深拷贝的自定义类型中,一般来说不会出现在深拷贝的自定义类型中:1>.深拷贝的自定义类型:如vector/string/map...,这些底层在进行拷贝时会进行深拷贝的类,实现移动构造和移动赋值是很有价值的。2>.浅拷贝的自定义类型:如Date/pair<int,int>...,这些底层在进行拷贝时会进行浅拷贝的类,不需要实现移动构造和移动赋值是有很大的价值的。浅拷贝的变量,编译器在进行构造时所消耗的代价是非常小的,它不像深拷贝那样,深拷贝的底层是不要在堆上开空间,这个操作的代价相对来说还是比较大的,不仅如此,我们前面在讲移动语义的时候,它的内部其实就是进行了交换操作,对于Date类型的对象来说,交换无非就是交换内部的数据,可是既然如此的话,大家不妨在这里想一想,浅拷贝和交换有区别吗?(交换其实也是通过拷贝来实现的)OK,本质上其实没有什么区别,就连运行效率实际上也差不到哪去,不仅如此,我们编译器最后还会进行优化操作,这样一来,那么对于浅拷贝来说不用移动语义的效率也还是挺不错的。

       最后,这里再来给大家补充一个知识点:我们在进行传值返回的过程中,它最终返回的是一个临时对象,这里我们需要注意一下:这个临时对象它的存储位置是在main栈帧中。

       右值引用和移动语义在传参中的提效:查看STL文档我们发现C++11以后容器中的push和insert系列的接口增加了右值引用版本;当实参是一个左值时,容器内部会继续调用拷贝构造进行拷贝,将对象中的资源全部拷贝到容器空间中的对象上;当实参是一个右值时,容器内部则会调用移动构造,将右值对象中的资源全部移动到容器空间的对象上。

  3.6 类型分类

       1>.C++11以后,进一步对类型进行了划分,右值被划分为纯右值(prvalue)和将亡值(xvalue)。

       2>.纯右值是指那些字面值常量或求职结果(没有名字的临时对象)。如:42、true、nullptr、或者类似str.substr(1,2)、str1+str2这样的传值返回函数的那个返回值,或者是整形a++、a+b等。纯右值和将亡值是在C++11中提出的,C++11中划分出来的纯右值这个概念等价于C++98中的右值。

       3>.将亡值是指那些返回右值引用的函数的调用表达式和转换为右值引用的转换的调用表达,如move(x)、static_cast<x&&>(x)等。

       4>.泛左值(glvalue),泛左值包括将亡值和左值。

       5>.有名字,就是泛左值;有名字,且没有被move,就是右值;有名字,且已经被move了,就i是将亡值;没有名字,且已经被move了,就是纯右值。

  3.7 引用折叠

       1>.C++中不能定义引用的引用,如int& && = i;我们在编译器中这样写的话会报错,但是我们可以通过模板或typedef中的类型操作可以构成引用的引用。

       2>.通过模板或typedef中的类型操作就可以构成引用的引用,但是C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,其它所有组合均会折叠成左值引用。

       3>.下面的程序就很好地展示了模板和typedef时构成引用的引用时的引用折叠规则,我们大家需要一个一个仔细地去理解一下(引用折叠这部分需要我们大家重点掌握)。

       4>.像f2这样的函数模板中,T&&x参数看起来右值引用参数,但是由于引用折叠的规则,当我们传递一个左值过去时就是左值引用,传递右值过去时就是右值引用,因此在有些地方也把这种函数模板的参数就做万能引用。

template<class T>
void f1(T&x)//按照我们上述的引用折叠的规则来看的话,f1实例化后的形参总是一个左值引用。
{ }
template<class T>
void f2(T&&x)//按照引用折叠的规则来看的话,f2实例化后的形参有可能是左值引用,也有可能是右值引用,具体是什么则由实参的类型来决定。
{ }
int main()
{
	//我们这里先用typedef来展示一下这个引用折叠的规则。
	typedef int& ref;
	typedef int&& rref;
	int n = 1;
	ref& r1 = n;//左值引用和左值引用被折叠成了左值引用,因此r1的类型是int&类型。
	ref&& r2 = n;//左值引用和右值引用折叠成了左值引用,因此r2的类型是int&类型。
	rref& r3 = n;//右值引用和左值引用折叠成了左值引用,因此r3的类型是int&类型。
	rref&& r4 = 1;//右值引用和右值引用折叠成了右值引用,因此r4的类型是int&&类型。
	//接下来,我们来通过f1和f2这两个函数模板来展示一下这个引用折叠的规则。
	f1<int>(n);//没有引发折叠,形参x的类型为int&。
	//f1<int>(0);//编译器会报错;和上述那句代码的解释相同,右值不能直接传给左值引用变量,因此这里会报错。
	f1<int&>(n);//左值引用和左值引用折叠成了左值引用,形参x的类型为int&。
	//f1<int&>(0);//编译器会报错;右值不能直接传给左值。
	f1<int&&>(n);//右值引用和左值引用折叠成了左值引用,形如x的类型为int&。
	//f1<int&&>(0);//编译器会报错;右值不能直接传给左值。
	f1<const int&>(n);//左值引用和左值引用折叠成了左值引用,const修饰它不会影响折叠的规则,形参x的类型为const int&。
	f1<const int&>(0);//编译器在这里不会报错;因为const左值引用可以引用右值。
	f1<const int&&>(n);//右值引用和左值引用折叠成了左值引用,形参x的类型为const int&。
	f1<const int&&>(0);//不报错;因为const左值引用可以引用右值。
	f2<int>(0);//没有折叠,形参x的类型为int&&。
	//f2<int>(n);//编译器会报错;左值不能直接传给右值。
	f2<int&>(n);//左值引用和右值引用折叠成了左值引用,形参x的类型为int&。
	//f2<int&>(0);//编译器会报错;左值引用和右值引用折叠成了左值引用,因此不能传右值过去。
	f2<int&&>(0);//右值引用和右值引用折叠成了右值引用,因此参数x的类型为int&&。
	//f2<int&&>(n);//编译器会报错;右值引用不能直接引用左值。
	return 0;
}

       在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++;//这行代码可以存在也可以不存在,具体看实际参数的类型。
}
int main()
{
	Function(10);//10是右值,编译器再这里会推导出T为int,模板被实例化为void Function(int&& t)。
	int a = 0;
	Function(a);//a是左值,编译器在这里根据引用折叠的规则会推导出T为int&,模板实例化为void Function(int& t)。
	Function(move(a));//move(a)会返回一个右值,也就是说,传过去的实参是右值,T被推导成int,模板会实例化为void Function(int&& t)。
	const int b = 10;
	Function(b);//b是左值,编译器在这里会推导出T为const int&(根据引用折叠),模板实例化为void Function(const int& t);因此第5行代码就不可以出现,t被const修饰了。
	Function(move(b));//move(b)会返回一个右值,也就是说,传过去的实参是一个右值,T被推导成const int,模板实例化为void Function(const int&& t),由于t被const修饰了,因此第5行代码在这里是不能出现的。
	return 0;
}

  3.8 完美转发

       1>.在Function(T&& t)这个函数模板程序中,传左值实例化之后是左值引用的Function函数,传右值实例化之后是右值引用的Function函数。

       2>.但是结合我们在前面3.2这个章节中的讲解可知,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性其实是左值,也就是无论我们实参的类型是左值还是右值,最终实例化出来的那个Function函数实参的属性永远都是左值属性,既然如此的话,那么我们把t传给下一层Fun函数时,那么匹配到的就都是左值引用版本的Fun函数。这里我们若是想要在传递给下一层函数Fun时保持t的属性不变的话,就需要使用完美转发去实现。

       3>.

template<class T>
T&& forward(typename remove_reference<T>::typed& arg);

       4>.

template<class T>
T&& forward(typename remove_reference<T>::typed&& arg);

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

void Fun(int& x)//1)
{
	cout << "左值引用" << endl;
}
void Fun(const int& x)//2)
{
	cout << "const左值引用" << endl;
}
void Fun(int&& x)//3)
{
	cout << "右值引用" << endl;
}
void Fun(const int&& x)//4)
{
	cout << "const右值引用" << endl;
}
template<class T>
void Function(T&& x)
{
	Fun(forward<T>(x));//forward<T>(x)这一步是完美转发。
}
int main()
{
	Function(10);//右值引用;10是一个右值,将10传过去的话,x是一个右值引用变量,既然如此的话,x的属性是左值,forward<T>(x)这个函数会将x这个右值引用变量强制类型转换成一个右值,并将其返回,这样的话Fun这俄格函数的实参是一个右值,因此会匹配到3)这个Fun函数。
	int a = 0;
	Function(a);//左值引用;a是一个左值,将a传过去之后,x就是一个左值引用变量,x的属性是左值,forward<T>(x)这个函数会将x这个左值引用变量强制类型转换为左值,将其返回,这样Fun函数的实参就是一个左值。
	Function(move(a));//右值引用;a是一个左值,move(a)这个操作返回的是一个右值,因此,Function的实参是右值,则x是一个右值引用变量,x的属性是左值,forward<T>(x)这个函数会将x这个右值引用变量强制类型转换为右值并返回。因此会匹配到3)。
	const int b = 0;
	Function(b);//const左值引用;b是一个被const修饰的左值,将b传过去之后,T被识别为const int&,x是一个const左值引用变量,forward<T>(x)这个函数会将x这个const左值引用变量强制类型转换为一个const修饰的左值并返回,这样Fun函数的参数是一个const修饰的左值。因此匹配到2)。
	Function(move(b));//const右值引用;b是一个被const修i是的左值,move(b)会将b转换成一个const修饰的右值,传过去之后,T被识别为const int,forward<T>(x)这个函数会将x这个被const修饰的右值引用变量强制类型转换为一个被const修饰的右值并将其返回,这样Fun函数的参数就是一个const修饰的右值,因此会匹配到4)。
	return 0;
}

       OK,我们讲到了这里,就说明我们这里的右值引用和移动语义在这里讲解就结束了,我们在最前面时讲到了右值引用变量它的属性时左值,这个知识点我们无法用理论去解释它(它是c++11的语法规定的),通过我们后续的讲解可知,将右值引用变量的属性设置为左值是有很大好处的,比如说可以用移动构造和移动赋值来抢夺右值变量中的资源,这样的话,极大地提高了程序运行的效率,减少资源的拷贝,......总之,将右值引用变量的属性弄成左值后,好处还是挺多的。

4 可变参数模板

  4.1 基本语法原理

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

       2>.

template<class ...Args>//模板参数包,"...Args"就是一个参数包。
void Func(Args ...args)//函数参数包,"...args"这个参数包表示每个参数都是传值传参,"Args"代表的是类型。
{ }
//在模板参数包中"..."写在类型的前面;在函数存在参数包中,"..."写在类型的后面,参数包的意思是参数的个数是零或多个。

       3>.

template<class...Args>
void Func(Args&...args) { }//每个参数都是左值引用传值。

       4>.

template<class...Args>
void Func(Args&&...args) { }//万能模板(引用折叠)

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

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

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

template<class...Args>//模板参数包,有零或多个模板参数。
void Print(Args&&...args)//函数参数包,表示有零个或多个函数参数。
{
	cout << sizeof...(args) << endl;//计算参数包中的元素个数。
}
int main()
{
	double x = 2.2;
	Print();//0;参数包里有0个参数。1>
	Print(1);//1;参数包里有1个参数。2>
	Print(1, string("xxxxxxx"));//2;参数包里有2个参数。3>
	Print(1, string("xx"), x);//3;参数包里有3个参数。4>
	Print(1, string("yy"), 2.2);//3;参数包里有3个参数。5>
	return 0;
}

       OK,上述操作就是我们对上述语法的一个简单解析与应用,那么会使用了之后,我们接下来就来看一看为什么这里要设计出这个可变参数模板呢?

       在讲解之前,我们先来为大家讲一讲当初设计函数模板的意图,例如我们写一个swap交换元素的这个函数,面对char、int、double...等多种不同类型的元素,我们就要写出多个不同参数类型的swap函数,为了方便,因而设计出了模板参数这个东西,它可以根据我们传过来的实参的类型来实例化出相对应的swap函数,最终我们调用的还是实例化出来的那个swap函数,但是,说到这里,我们大家需要来注意一下,就是函数模板它只能实例化出参数个数相同的函数,就拿上面的Print这个函数模板来说(假设Print函数模板参数为3个),Print这个函数只能实例化出4>和5>这两个参数个数为3个的参数,对于1>、2>、3>这3个Print函数它是实例化不出来的,这里我们若想实例化出1>、2>和3>出来,就需要我们再单独写一个参数个数为0个的Print函数以及参数个数分别为1个和2个的Print函数模板,那么,既然如此的话,对于参数个数为1,2,3,4...的同一函数而言的话,我们就还需要自己去写参数个数分别为1,2,3,4...的同一不同参数的函数模板,显然还是很麻烦,因此C++11才会在这里设计出这个可变参数模板这个好东西,我们先来看下图:

       好了,上面那幅图就可以很好地展现出我们这里的关系,由于函数模板它在这里只能实例化出参数相同且参数类型不同的Print函数,无法满足我们这里的需求,因此才有了可变参数模板,顾名思义,可变参数模板它的参数是可变的,也就是参数的个数是不确定的,就上图而言,可变模板可以实例化出多个参数不同的Print函数的模板,这里从而就很好地解决了上面地那个函数模板所凸显出来的那个问题。

       为了大家更好地去理解这里的这个知识,我们以4>这行代码为例,来为大家做一个超详细地讲解过程,Print现在还是一个可变参数模板,我们往Print这个可变参数函数模板中传了3个实参过去,那么这个Print可变参数函数模板便会根据参数包中元素的个数解析先实例化出一个参数个数为3的Print函数模板,实例化好后编译器又会根据Print函数模板从而实例化出一个参数个数为3的个且这3个参数的类型均为传过来的哪3个实参的类型的Print函数,最终调用的就是最后实例化出来的那个Print函数。编译器在这里推演的过程中其实还会结合引用折叠的相关规则,就拿4>和5>这两行代码来举例说明,4>的最后一个实参是左值,那么最终实例化出来的Print函数中最后一个参数的类型就是int&类型,而5>的最后一个参数是右值,则Print函数的最后一个参数的类型就是int&&类型。

  4.2 包扩展

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

       2>.C++还支持更为复杂的包扩展,直接将参数包一次展开一次作为实参给给另一个函数去处理。

       我们接下来来实现一下将参数包中的所有元素均一一打印出来,代码如下:

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, string("xxxxxx"), 2.2);//1 xxxxxx 2.2;以上代码就是我们这里所实现的输出包中元素的代码,这里我先来讲解一下传参的过程:首先Print函数中的三个实参会全部传给参数包,在Print函数的内部将参数包作为实参传给了ShowList这个函数,在传的过程中,参数包中的第一个参数会传给x,另外两个参数传给第二个参数包;在ShowList函数内部中,先将x打印出来,然后将第二个参数包再用递归的方式再次传给ShowList函数,在传的过程中,参数包中的第一个参数会传给x,另外的那个参数会传给第二个参数包;将x输出来,再去调用ShowList函数,再传的过程中,参数包中的那个参数会传给x,输出,然后再去调用ShowList函数,要注意的是,此时的参数包中已经没有参数了,也就是说,这次调用的ShowList函数是没有参数的,那么匹配到的就是前4行的这个ShowList函数。
	return 0;
}

       既然如此的话,那么有的同学可能就要在这里问了,为什么不直接在ShowList函数的内部写一个判断语句,去判断这这个包中的参数的个数,如果数目为0,则直接return结束呢?这样做不是更方便吗?因为我们包扩展在这里的逻辑体现的是编译时逻辑,并非是运行时逻辑,上面那位同学所说的思路逻辑属于运行时逻辑,在这里是行不通的。换句话说,我们在上述的代码中所实现的递归方式属于编译时逻辑,而那位同学所说的那种情况属于是运行时逻辑的结束方式,这两种递归有本质上的区别,不可一蹴而就,既然说到这里,那么我们就再来补充一个新的知识,就是怎么样判断一个递归是编译时递归还是运行时递归呢?其实对于这种问题的话,我们这里其实不用去专门判断,编译时递归主要就几种常见的案例而已,但是为了更好的回答这个问题,我们暂时可以认为只要递归时参数的个数在不断变化,那么就说明这个递归方式时编译时递归,在递归时参数的个数不变的就说明次递归是运行时逻辑。OK,既然我们现在已经得知了上述代码中的递归方式为编译时递归了,那么接下来我们就上述代码来看一看它的推导过程吧。

       OK,好了,上幅图中的那个代码就是在编译阶段中生成的代码,这里我们来为大家解释一下为什么不能写那个运行时递归的结束逻辑,我们上述的代码在编译这个阶段就会将编译时逻辑转换成代码体现出来,是一段完整的逻辑思路,如果我们不写void ShowList()这个函数的话,那么编译器在调用ShowList()这个函数是就会报编译错误。

       上述所讲的包扩展的方法只是其中的一种方法,第二种包扩展的方法则是可以直接将编译阶段的那个代码直接写出来,不用让编译器自己去实现得到,但是只要无异于是多管闲事,不仅不方便,反而还添乱,因此第二种方法不建议使用。

       除了上述这里所说的哪两种扩展的方法之外,这里其实还有另外的第三种扩展方法,接下来,我们来跟着代码一起看一下:

template<class T>
int GetArg(const T& x)
{
	cout << x << " ";
	return 0;//GetArg这个函数必须要有返回值,至于返回什么可以不用管。
}
template<class...Args>
void Arguments(Args...args)
{ }
template<class...Args>
void Print(Args...args)
{
	Arguments(GetArg(args)...);//注:这里的GetArg函数必须要返回得到的对象,只有这样才能组成参数包给给Arguments函数。
}
int main()
{
	Print(1, string("xxxxxx"), 2.2);//1 xxxxxx 2.2;用我们上述所给的代码,这里也可以完成扩展的操作,我们这里来为大家细细地过一遍,我们传了3个实参传给Print函数中的参数包,在Print函数的内部,这里调用Arguments函数,这俄格函数地参数也是一个参数包,不同的是,这个参数包是GetArg函数地返回值所组成地参数包,也就是说,在调用Arguments函数之前,编译器会将传给Print函数的参数包中的每一个参数都作为实参传给GetArg函数,得到这些GetArg函数地返回值,这些返回值又会作为一个新的参数包去传给Arguments函数,至于输出数据地操作,是在每一次调用GetArg函数时就将其给输出来了。换句话说,就是编译器在编译过程中,将Print这个函数模板给实例化成了如下代码:
	//void Print(int x,string y,double z)
	//{
	//	Arguments(GetArg(x), GetArg(y), GetArg(z));
	//}
	//大家在看到这个实例化之后的Print代码后,思路是不是一下子就被打开了,编译器在这里会一次以x、y、z为参数去调用GetArg函数,从而将参数包中的数据一一输出来。
	//说到最后,我们这里再来给大家讲一个问题,就是代码中的GetArg这个函数为什么一定要有返回值呢?因为有的同学在看到13这行代码时,如果GetArg这个函数没有返回值的话,那么13这行代码中Arguments的参数包就为0(通过我们前面的学习可知,参数包时可以为0的),这样的话,按照理论不是也可以成功地调用Arguments函数吗?对于这个回答,如果我们大家去仔细思考的话,却感觉也挺正常的,但是当我们用上述思路去实际运行的时候,编译器会报错,原因是没有实例化出相对应的Arguments函数,其实对于这个问题我也说不清楚,但是我们这里推测这里之所以会出现这个报错信息是因为编译器它在进行编译的时候,其实就已经根据传过去的参数包中的个数将Print函数实例化好了,(这里我们以上述代码为例来作解释)实例化成了18到21这4行代码,因此,Arguments函数有3个参数,编译器就会根据Print函数中的Arguments这个函数的3个参数从而实例化出了拥有3个参数的Arguments函数(由8到10这几行代码实例化出),那么,这样的话,那个GetArg函数就必须要有返回值了,如果GetArg这个函数没有返回值的话,则8到21这4行代码中的Arguments函数就没有参数,但是前面编译器并没有根据8到10这几行代码实例出无参的Arguments函数,进而无法找到相对应的函数,因此编译器在这里会报错。
	return 0;
}

  4.3 emplace系列接口

       1>.

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

       2>.

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

       3>.在C++11以后STL容器就新增了emplace系列的接口,emplace系列的接口均为模板可变参数,功能上兼容push和insert系列,但是emplace它还支持新玩法,假设容器为container<T>(模板,诸如list这样内部每次存储的类型可以不同的容器)的话,emplace还支持插入构造T对象的参数,这样的话在有些场景中会更为高效一些,可以直接在容器空间上构造T对象。

       4>.emplace_back总体而言是更为高效的,推荐以后使用emplace系列替代insert和push系列。(我们接下来就利用前面所学过的list类中的emplace_back和push_back这两个函数接口来看一看它们的效率是如何的?)

class string
{
public:
	string(const char* str = "")
		:_str(strlen(str))
		, _capacity(_size)
	{
		cout << "构造" << " ";
		_str = new char[_capacity + 1];
		strcpy(_str, str);
	}
	string(const string& s)
		:_str(nullptr)
	{
		cout << "拷贝构造" << " ";
		//...
	}
	string(string&& s)
	{
		cout << "移动构造" << " ";
		//...
	}
	//...
private:
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;
};//我们在下面的代码中用的是上面刚刚写的这个string类,而list用的是STL库中的那个。
int main()
{
	list<string> lt;
	string s1("111111");//构造。
	string s2("222222");//构造。
	//传左值过去,跟push_back一样,走拷贝构造。
	lt.emplace_back(s1);//拷贝构造;(这里我们参考STL库中的list库中的emplace_back和push_back这两个函数接口来作解释)s1是一个左值,那么emplace_back函数的参数包中只有一个参数,这样Args就会被识别为string&类型,这样的话,最终传给list的构造函数的就是一个左值,那么,在list的构造函数的内部就会去调用string类的拷贝构造去构造一个与传过来的左值一模一样的string类型的对象。
	lt.push_back(s2);//拷贝构造。
	//通过上面的两行代码我们可以得知,传左值的话,emplace_back和push_back在效率上是没有什么区别的,都一样。
	lt.emplace_back(move(s1));//移动构造;这一次我传过去的是一个右值,这样的话,Args会被识别为string类型,使得最后传到list构造函数的是一个右值,那么,在list的构造函数的内部,会调用string类的移动构造函数,回去掠夺右值中的资源从而构造一个新的string类型的对象作为list节点中存放的元素。
	lt.push_back(move(s2));//移动构造;
	//通过紧接着的上面两行代码,我们可知,传右值的话,emplace_back和push_back的运行效率是一样的,没有差距。
	lt.emplace_back("111111");//构造;通过我们对list库中的emplace_back函数接口的观察,我们会发现emplace_back它是一个函数模板,至于它的参数的类型,是由参数的类型所决定的,并不是由lt对象中所存储的元素的类型所决定的,我们这行代码传给emplace_back这个函数模板的是一个常量字符串,它的类型是const char*,因此,就会推导出Args是const char*类型的常量字符串,那么,编译器在这里直接根据"111111"这个常量字符串去构造一个string类型的对象作为list节点中存放的元素。
	lt.push_back("111111");//构造 移动构造;如果我们大家仔细地去看list库中的push_back这个函数接口的话,会发现它的参数的类型是lt对象中所存储的元素的类型,也就是说,编译器在执行这行代码之前,会先将"111111"这个常量字符串隐式类型转换成一个string类型的临时对象,会调用void push_back(value_type&& val);这个函数,这样的话,最终是会调用移动构造去掠夺那个传过来的临时对象中的资源,从而构造一个新的string类型的对象来作为list节点中所存储的元素。
	//通过紧接着上面的两行代码,我们可以得知,对于深拷贝的类型而言,emplace系列相对而言少了一个移动构造的步骤,因此,运行效率会稍微快一点;说到这里,我们就也来探讨一下浅拷贝的类型,如果是浅拷贝的话,对于emplace_back而言,还是只调用一次构造函数,对于push_back函数而言,也会调用构造函数去构造一个临时对象,只不过不会再调用移动构造了,而是会去调用拷贝构造(拷贝构造的速度很快),经过这样一对比的话,我们会发现对于浅拷贝而言,emplace系列的运行效率会显得更快一点。
	return 0;
}

       5>.接下来我们再来看一个程序,我们这里会模拟实现list的emplace中的emplace_back函数接口,这里会把参数包不断地往下去传递,最终在节点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面所说的emplace支持插入构造T对象的参数,这样的话在有些场景下会显得更为高效一些,可以直接在容器空间上构造T对象。

template<class T>
class list_node
{
public:
	T _data;
	list_node<T>* _next;
	list_node<T>* _prev;
	list_node() = default;//编译器强制实现默认构造函数,如果我们大家要想实现一个不存放任何元素的节点的话,那么我们就不能通过我们自己所实现的那个构造函数来实现,只能让编译器强制实现一个默认构造来实现,这时候可能有同学就会问了,默认构造函数不就是参数为0吗?那么这样的话,我们可以认为参数包为0啊?OK,其实这位同学的理解完全是对的,但是编译器在这里是不支持的,在VS这个编译器这里,我们给参数包传参时,最少都要传1个参数过去,否则会报错,这个大家适应一下吧。
	template<class...Args>
	list_node(Args..args)
		:_next(nullptr)
		,_prev(nullptr)
		,_data(forward<Args>(args)...)//3>
	{ }
};
template<class T>
class list
{
	typedef list_node<T> Node;
public:
	template<class ...Args>
	void emplace_back(Args&&...args)
	{
		insert(end(), forward<Args>(args)...);//1>
	}
	template<class ...Args>
	iterator insert(iterator pos, Args&&... args)
	{
		//...我们这里只写比较重要的几行代码,其余的我们这里均用"..."代替即可。
		Node* newnode = new Node(forward<Args>(args)...);//2>
		//...
	}
	//...
private:
	Node* _head;
	size_t _size;
};
int main()
{
	list<pair<string, int>> lt;
	pair<string, int> kv("zyb", 1);//构造。
	lt.emplace_back(kv);//拷贝构造;和push_back一样。
	lt.emplace_back(move(kv));//移动构造;和push_back一样。
	lt.emplace_back("zyb", 1);//构造;我们这里的这个emplace_back函数的参数为"zyb"和1,它们会传给参数包(emplace_back的参数),在emplace_back这个函数的内部,将这个参数包原封不动地传给insert函数,也就是1>这行代码,再来看insert函数的内部,会根据传过来的那个参数包去构造一个T类型地对象作为节点中存放的元素,也就是2>这行代码,调用节点的构造函数,在3>这行代码所描述的这个步骤时,编译器会根据T的类型,从而将参数包中的参数转换为T类型的对象。
	//lt,emplace_back({"zyb",1});//编译器会报错;编译器它是不支持这样去传参的,因为它不走隐式类型转换,因为编译器不知道Args是什么,在自定义类型中,不只有pair类型有两个变量,还有很多类型的成员变量的个数也是两个,因此不支持这样传参。
	lt.push_back({ "zyb",1 });//push_back支持这样传参,{ "zyb",1 }会隐式类型转换为一个pair类类型的对象。
	//lt.push_back("zyb",1);//相反,push_back不支持类似这样传参,因为编译器在这里经过实例化操作实例化之后,push_back的参数是一个pair类类型的对象,而这句代码是穿了两个参数是一个pair类类型的变量,而这句代码是传了两个实参过去,会匹配不全。因此不支持这样传参。
	return 0;
}

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

       OK,讲到这里,就说明我们对所有的可变参数模板知识点全部讲解结束了,最后,我们这里再来一个总结:

       总结1:emplace系列兼容push系列和insert的功能,在部分场景下emplace可以直接构造,push和insert则是构造+移动构造 / 构造+拷贝构造,所以综合而言emplace则更好用更强大,因此我们推荐大家去用emplace系列替代push和insert系列。

       总结2:    {函数模板,一个函数模板可以实例化出多个不同类型的函数。
                      {可变参数模板,一个可变模板参数可以实例化出多个不同参数个数的模板。

5 lambda

  5.1 lambda表达式语法

       1>.lambda表达式本质上是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。lambda表达式在语法使用层而言没有类型,或者说在语法层上我们不认识它的类型,所以我们一般是用auto或者模板参数定义的对象去接收lambda对象。

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

       3>.[ capture-list ] :捕捉列表,该列表总是出现在lambda函数的开始位置,编译器在这里是根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量以供lambda函数去使用,捕捉函数可以传值和传引用捕捉,至于具体的细节我们会在7.2章节中去讲。注:即便捕捉列表为空也不能将其省略掉。

       4>.( parameters ) :参数列表,与普通函数的参数列表的功能类似,如果不需要我们传递参数的话,则这里是可以连同"()"一同省略掉的。

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

       6>. { function body }:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。注:函数体即便为空也不能省略这部分。

int main()
{
	auto add = [](int x, int y)->int {return x + y; };//这是一个lambda表达式,通过我们前面的学习,得知它其实就是一个对象,至于它的类型是什么,在语法层面上我们是不得而知的(后面会细讲为什么不知道它的类型),上面这句代码是将这个对象传给add,那么,add代表的就是那个对象,至于add的类型是什么,我们让编译器自己去推导即可。因此,我们就可以直接通过add对象去调用了。
	cout << add(1, 2) << endl;//3;调用成功,并打印出返回值3.
	auto func1 = []//可以省略参数。     //我们这里也可以像平时写函数那样将实现部分分开写。
		{
			cout << "hello world" << endl;
			return 0;
		};
	func1();//hello world
	return 0;
}

  5.2 捕捉列表

       1>.lambda表达式中默认只能用lambda函数体和参数中的变量,如果像用外层作用域中的变量的话就需要进行捕捉。

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

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

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

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

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

int a = 10;
int main()
{
	int b = 20;
	int c = 30;
	auto add1 = [c, &b](int x, int y)//对c进行传值捕捉,对b进行传引用捕捉。
		{
			cout << ++a << " ";//对于静态局部变量和全局变量来说,这两种类型的变量不用我们特意的去捕捉,它们可以直接使用,它们两个的使用方法和通过传引用捕捉过来的对象的使用方法基本是一样的,是允许被修改的。
			//c++;//我们对c这个变量是通过传值捕捉的方式捕捉过来的,由于lambda捕捉列表是被const修饰过的,因此c是不允许被修改的。
			b++;//我们对b这个变量是通过传引用捕捉的方式捕捉过来的,它是允许修改的,说到这里,或许有的同学就会有一些疑问,就是我们前面讲到的lambda的捕捉列表是被const修饰过的,既然捕捉列表是被const修饰过的,那么这样说的话,捕捉过来的所有变量都应该是要被const修饰的,但是为什么只有c受影响,而b不受影响呢?对于这个问题,其实我也做不了太过细致的讲解,对于这个语法问题,大家在这里没有必要去了解的那么细致,这里我们可以认为是lambda捕捉列表它只对传值捕捉的变量由const修饰,对传引用捕捉的变量丝毫没有影响,也就是说const只修饰被传值捕捉的方式捕捉过来的变量,不作用于被传引用捕捉的方式捕捉过来的变量,大家暂时就先这样理解吧。
			//d++;//d这个变量是在lambda表达式之后定义的,而lambda捕捉列表它只能捕捉定义在lambda表达式之前的局部变量,因此它捕捉不到d。
		};
	add1(1, 2);//11;
	cout << a << b << endl;//11 21;a和b都是通过传引用捕捉的方式被lambda的捕捉列表给捕捉到的,因此形参改变,实参自然而然也就会跟着改变。
	auto add2 = [=, &c](int x, int y)
		{
			//[=, &c]的意思就是对lambda这个表达式所在的那个域中且定义在lambda表达式之前的变量用的是传值捕捉的方式,但是对c这个变量用的是传引用捕捉的方式。
			//b++;//b不可修改,它是被传值捕捉的方式捕捉过来的。
			c++;//c可修改。
		};
	add2(1, 2);
	cout << c << endl;//31
	auto func1 = [&, b](int x, int y)mutable//加上了mutable这个关键字之后,就可以取消被用传值捕捉过来的变量的常量性了。
		{
			//[&,b]的意思就是说,对lambda这个表达式所在的那个域中且定义在lambda表达式之前的变量用的是传引用捕捉的方式,但是对b这个变量用的是传值捕捉的方式。
			c++;//c可以修改。
			b++;//b这个变量本来不可以被修改,但是加上了mutable这个关键字后,b这个变量就可以修改了。
			a++;//a也可以修改。
		};
	func1(1, 2);
	cout << b << endl;//21;b是被传值捕捉的方式捕捉过去的,即便可以修改了,那也只是形参对象被修改了,实参对象没有一点变化。
	return 0;
}

  5.3 lambda的应用

       1>.在学习lambda之前,我们之前所使用可调用的对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦一点,而仿函数要定义一个类相对来说比较麻烦。使用lambda去定义可调用函数,既简单又方便。

       2>.lambda在很多其他地方用起来也非常好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda的应用还是很广泛的,以后我们会不断地接触到。

struct Goods
{
	string _name;//姓名。
	double _price;//价格。
	Goods(const string& name, const double& price)
		:_name(name)
		,_price(price)
	{ }
};
struct Compare1
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._price > g2._price;
	}
};
struct Compare2
{
	bool operator()(const Goods& g1, const Goods& g2)
	{
		return g1._price < g2._price;
	}
};
int main()
{
	vector<Goods> v = { {"苹果",5},{"香蕉",6} };
	//我们接下里来对v中所存储的数据按照_price的大小来排一个顺序,就从大到小排吧。
	sort(v.begin(), v.end(), Compare1());//编译器在执行完这一句代码后,那么v中的元素就以_price按照从大到小的顺序就排好了,这个仿函数Compare1的顺序是只有我们才知道,倘若此时来了一个编程小白,他想使用我们上述这里所写的这个代码,但是他不知道这个Compare1这个仿函数是实现升序的效果还是实现降序的效果,因此,他还要专门去找到Compare1这个类去了解一下,这样做的话,感觉稍微有点过于麻烦了,此时若将Compare1换成前面刚刚讲过的lambda表达式的话,就可以一目了当的知晓这里实现的是升序还是降序操作了。
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });//我们使用这样的话,那么为例是谁都可以看到这一步实现的是升序还是降序了。
	return 0;
}

  5.4 lambda的原理

       1>.lambda的原理其实和范围for很像,编译后从汇编指令层的角度去看的话,压根就没有lambda和范围for这样的东西。范围for的底层是迭代器,而lambda的底层是仿函数对象,也就是说我们写了一个lambda以后,编译器在这里则是会生成一个对应的仿函数的类。

       2>.仿函数的类名是编译按一定的规则生成的(至于这个规则具体是什么,这里我们暂时先不做介绍),保证不同的lambda生成的类名是各个不同的,lambda参数/返回值类型/函数体其实就是仿函数中的operator()的参数/返回值的类型/函数体,lambda的捕捉列表本质上是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉也是这个道理,和这所讲的内容大差不差,只不过这里并不会将全部符合条件的变量都捕捉过去,而是编译器在这里会看使用哪些就传哪些对象。

       3>.对于上面的原理,我们下面稍微用几句代码讲解即可,如果大家有同学想要理解的更为深刻一点的话,可以通过汇编层去了解一下。

int main()
{
	double rate = 0.49;
	auto r2 = [rate](double money, int year)
		{
			return money * rate * year;
		};
	//OK,上述就是我们这里所写的一个lambda表达式了,接下来我们就来仿照编译器将这个lambda表达式转换成一个仿函数。
	//class lambda_15431254315bhbj4651342432//类名是按某个规则形成的。
	//{
	//private:
	//	double _rate;
	//public:
	//	lambda_15431254315bhbj4651342432(double& _rate)
	//		:rate(_rate)
	//	{ }
	//	bool operator()(double money, int year)
	//	{
	//		return money * year * rate;
	//	}
	//};
	//以上就是我们在这里模拟实现的lambda表达式,这样的话,第4到7行代码就会变成如下所示:
	//auto r2 = lambda_15431254315bhbj4651342432(0.49);//lambda_15431254315bhbj4651342432(0.49)是一个匿名对象。
	return 0;
}

6 STL中的一些变化

      1>.array、forward_list、unordered_map、unordered_set这四个容器就是我们在C++11后STL中新增加的四个容器,别看一下就新增了这么多的容器,但是实际上最有用的容器是unordered_map和unordered_set这两个容器。而这两个容器我们在前面已经对其进行了十分细致的学习了,其他的可以结合相关网站去了解一下即可。

       2>.相应的,C++11后,STL中容器的新增接口也增加了不少,最重要的其实就是右值引用和移动语义相关的push/insert/emplace系列的接口和移动构造和移动赋值,还有initializer_list版本的构造等等,这些我们都在前面讲过了,还有一些无关痛痒的,需要时查查文档即可。

       3>.容器的范围for遍历,这个在容器部分也讲过了。

7 新的类功能

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

       1>.在C++之前的类中,每个类都会有6个默认成员函数:构造函数 / 析构函数/拷贝构造函数 / 拷贝赋值重载 / 取地址重载 / const取地址重载,其中最为重要的是前4个,后两个用处不是很大,默认成员函数就是我们不写编译器会自动生成一个默认的。C++11又新增了两个默认成员函数,移动构造和移动赋值运算符重载。

       2>.如果我们没有自己实现移动构造函数,且没有实现析构函数,拷贝赋值重载中的任意一个的话,那么编译器它会自动去生成一个默认移动构造函数。默认生成的移动构造函数,对于内置类型成员来说的话会执行逐成员按字节拷贝,对于自定义类型来说的话,则需要看这个成员是否实现了移动构造,如果实现了的话就调用移动构造,没有实现的话则是去调用拷贝构造函数。

class string
{
public:
	string()
	{
		cout << "构造函数" << endl;
	}
	string(const string& s)
	{
		cout << "拷贝构造函数" << endl;
	}
	string(const string&& s)//1>
	{
		cout << "移动构造" << endl;
	}
	void operator=(const string& s)
	{
		cout << "拷贝赋值构造" << endl;
	}
	void operator=(const string&& s)//3>
	{
		cout << "移动赋值" << endl;
	}
private:
	//...
};//我们在下面的代码中所使用的string类均为上述我们刚刚写的那个string类。
class person
{
public:
	//...
	~person()//2>,默认person类中没有这两行代码。
	{ }
private:
	std::string _name;
	int _age;
};//我们在person这个类中既没有实现移动构造,也没有实现析构函数、拷贝构造和拷贝赋值重载中的任意一个函数,就说明符合编译器默认生成一个移动构造函数的条件,因此编译器会自动去生成一个默认移动构造函数。
int main()
{
	person p1;//构造函数;
	person p2 = p1;//拷贝构造函数;
	person p3 = move(p2);//移动构造;这句代码的意思是用一个右值去给p3进行初始化操作,既然是右值,那么就会调用编译器自己生成的那个默认移动构造函数,由于我们在string类中已经实现了移动构造函数,因此在对p3中的_name进行初始化时,会去调用string类中的移动构造函数。
	//接下来我们将1>这段代码给删除掉。
	person p4 = move(p2);//拷贝构造函数;
	//接下来我们再加上1>这段代码和2>这段代码。
	person p5 = move(p2);//拷贝构造函数;2>这段代码是关于person类的析构函数相关的代码,通过条件约束,编译器在这里则不会去为person类生成默认移动构造函数,因此我们无论是用什么值去初始化,在对_name变量进行初始化时,都会调用参数为左值的那个构造,也就是拷贝构造函数。
	return 0;
}

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

       4>.如果你提供了移动构造或移动赋值的话,编译器则不会自动提供拷贝构造和拷贝赋值,它们之间是会互相影响的(这里就和构造函数差不多,当我们写了拷贝构造函数之后,编译器就不会再默认生成构造函数了)。

int main()
{
	person p1;//构造函数;
	person p2;//构造函数;
	p2 = move(p1);//移动赋值;这几句代码的意思是将一个右值赋值给给另一个左值,既然是右值,而且我们在person类中实现析构函数、拷贝赋值、拷贝构造中的任意一个函数,且没有实现移动赋值重载函数,因此在这里会选择去调用编译器自己生成的默认移动赋值重载函数,由于我们在string类中已经实现了移动赋值,因此在对p2中_name变量进行赋值操作时,会调用string类中的移动赋值。
	//后续的过程由于时间原因,我们这里就不写了,跟前一个代码的相对应的部分解释差不多(将"1>"替换成"3>"就可以了)。
	return 0;
}

  7.2 声明时给缺省值

       这个知识我们在C++入门基础部分章节都已经讲过了,忘了的话大家就去复习一下吧。为了方便大家找,链接我就放到下面了:C++入门基础-CSDN博客

  7.3 default和delete

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

       2>.如果我们这里想要限制某些默认函数的生成,该如何做呢?在C++98中,是将该函数设置成private,并且就单单只是声明而已,这样的话只要其他人想要调用的话就会报错,而且将其声明一下,那么编译器也就不会默认生成了(之所以将其设置成私有状态,是防止有人在类外独自将其实现出来,从而去调用)。在C++中则会更简单,只需在该函数声明时加上=delete即可,该语法指示编译器不擅长对应函数的默认版本,称=delete修饰的函数为删除函数。

class string1
{
public:
	string1(const string& s)
	{
		cout << "拷贝构造" << endl;
	}
	//string1() = default;//默认这句代码是不存在的。1>
	//...
private:
	int _name;
	int _age;
};
class string2
{
public:
	string2(int _name, int _age) = delete;
private:
	int _name;
	int _age;
};
int main()
{
	//string1 s1;//会报错;因为没有默认生成构造函数。加上1>这句代码的话,那么编译器就不会报错了,因为这里会强制生成构造函数。
	//string2 s2(2, 8);//编译器会报错;因为编译器不会生成参数个数为2个的构造函数的默认版本。
	return 0;
}

  7.4 final和override

       这个我们在多态章节就已经进行了细致且详细的讲解,如果大家忘了的话可以去复习一下,为了方便大家找,链接我就放到下面了:C++草原三剑客之一:多态-CSDN博客

8 包装器

  8.1 function

template<class Ret, class... Args>
class function<Ret(Args...)>;//Ret是返回值类型,Args是包装的那个对象的参数。

       1>.std::function是一个类模板,同时也是一个包装器。std::function它所实例化出来的那个对象可以包装存储其他的一些可调用对象,这其中就包括函数指针、仿函数、lambda、bind表达式等等,被存储的那个可调用对象被称之为是std::function的目标。若std::function不含有目标的话,则称它为空,调用空std::function的目标会导致抛异常。

       2>.函数指针、仿函数、lambda等可调用对象的类型千奇百怪,各不相同,std::function的优势就是可以统一它们的类型,对它们都可以进行包装,这样的话,在很多地方就可以很方便地去声明可调用对象的类型了。

#include<functional>//我们接下来要用function包装器去展示,那么就需要包含这个头文件。
using namespace std;
int f(int a, int b)//普通函数
{
	return a + b;
}
struct Functor//仿函数
{
public:
	int operator()(int a, int b)
	{
		return a + b;
	}
};
class Plus//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包装器可以包装各种可调用的对象。
	function<int(int, int)> f1 = f;//function类类型的对象f1包装了一个函数指针,这里需要给大家补充一个小小的知识,就是C++语法规定函数名也是一个函数指针,因此大家在这里可以看到的是f1包装了一个函数名?其实不是,而是f1包装了一个函数指针。
	function<int(int, int)> f2 = Functor();//function类类型的对象f2包装了一个仿函数对象。
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };//function类类型的对象f3包装了一个lambda类型的对象。
	//function<int(int, int)>;//我们在这里先来为大家讲解一下模板参数的意义,第一个int代表的是包装的那个可调用对象的返回值的类型,(int, int)代表的是包装的那个可调用对象的参数(形参)类型,需要我们大家去注意的是:包装的那个可调用对象的返回值的类型和参数一定要和这里包装器中的第一个int和(int, int)相对应上,否则会报错或者可能会报出一些警告出来(警告主要是由于类型转换导致的)。
	cout << f1(1, 1) << endl;//2;
	cout << f2(2, 2) << endl;//4;
	cout << f3(3, 3) << endl;//6;我们这里可以直接通过包装器类型的对象去调用所包装的那个可调用的对象。
	//好,那么接下来就它的基本原理来为大家简单地讲解一下这里的调用过程(我们接下来的讲解过程就以第36行代码为例子来进行讲解吧),简单来说,第36行代码实际上就是将f这个对象赋值给了f1,通过我们的学习可知,function其实就是一个类模板,同时也是一个仿函数,它的内部重载了一个operator()的成员函数,除此之外,还有一些其它的成员变量,至于这句代码,大家可以理解为function类的内部实现其实还定义了一个成员变量,这个成员变量的类型不是固定的,它的类型是由被包围的那个可调用对象的类型决定的,而那个赋值其实用f给f1对象中的那个成员变量进行初始化操作,OK,说到这里,就说明第36行代码我们已经解释完了,接下来看第40行这句代码,也就是"f1(1,1)"这个操作,由于function它是一个仿函数,因此在其内部是重载了operator()这个成员函数的,也就是说,f1(1,1)在这里调用的是function类中的operator()这个成员函数,在operator()这个成员函数的内部,大家可以理解为是去调用那个可调用对象(成员变量),可调用对象的参数就是传过来的那个参数(1,1),最后执行函数,然后输出返回值。
	//OK,我们的上述展示了function包装器包装全局函数的过程,那么接下来,我们就来看一下包装成员函数的过程(以前面写的那个Plus类为例来作解释),包装成员函数的方法和包装普通函数的方法是一样的。
	//成员函数在这里又分为两种,一种是叫作静态成员函数,而另一种则为非静态成员函数,之所以在这里要将成员函数再细分一下,是因为这两种成员函数的取地址方法各有不同(之所以这里要取地址,是因为成员函数的包装方法和普通函数的包装方法如出一辙,都是包装一个函数指针类型的变量)。
	//包装静态成员函数
	function<int(int, int)> f4 = &Plus::plusi;//对于成员函数来说,要想得到它的地址的话,是需要我们指定的作用域的,以及在最前面还需要加上&才能取到相应的成员函数的地址,我们的这句代码展示的是静态成员函数的包装过程,对于静态成员函数来说,我们不加&也可以取到相应的地址。
	function<int(int, int)> f5 = Plus::plusi;//编译器不会报错;
	//包装非静态成员函数
	//function<double(double,double)> f6 = &Plus::plusd;//编译器会报错;原因不是取地址的问题。
	//function<double(double,double)> f7 = Plus::plusd;//编译器会报错;对于非静态成员函数来说,在包装时,必须加&才可以取到相应函数的地址,如果不加&则取不到相应函数的地址,编译器会报错。
	//OK,讲到这里,接下来我们就来讲讲第50行那句代码报错的问题,如果我们大家仔细去看报错原因的话,发现报错原因竟是说这里的参数不匹配,为什么呢?大家不妨先想一想,我们第50行那句代码,这里时包装一个非静态成员函数,通过我们前面在类和对象章节的讲解可知,每一个非静态成员函数除了它本省所含有的那些参数外,还有一个隐藏的参数,叫做this指针,注:this指针这个参数是存在的,只不过被编译器给隐藏了而已,因此我们是看不见的,综上所述,我们在写包装器对象的类型时,如果包装的是非静态成员函数的话,还要注意应该要多加上一个参数类型,ru:
	function<double(Plus*,double, double)> f8 = &Plus::plusd;//编译器没有报错;
	Plus pl;
	cout << f8(&pl, 1.111, 1.1) << endl;//22.11;除了上面的这一种写法之外,还有下面的这种写法:
	function<double(Plus, double, double)> f9 = &Plus::plusd;//编译器并没有在这里报错,就说明这样写其实也是可以的,说到这里,有的同学在这里就会感到一些疑惑,就是非静态成员函数中隐藏的那个参数难道不是指针类型吗?不应该用一个指针类型的对象来接收过来的实参吗?为什么这里写一个对象类型的参数也可以呢?要想解决这位同学的问题,我们首先需要补充一个知识点:用".*"操作符去回调成员函数(这部分知识本来是在类和对象部分就要讲一下的,但是由于某种原因,我们当时知识说明了一下,并没做太过清晰的讲解,因此我们在这里的讲解也就相当于是对前面知识的一个补充)
  /*
    我们在前面的类和对象部分讲过,我们如果想要通过成员函数指针去回调相对应的成员函数时,要使用".*"。
	class A
	{
	public:
	    void func()
		{
		    cout<<"A::func()"<<endl;
		}
		void print()
		{
		    cout<<"A::func()"<<endl;
		}
	};
	typedef void(A::*PF)();//通过我们前面对函数指针的学习,我们可知,函数指针的类型是非常麻烦的,因此我们这里将类型typedef一下,这句代码的意思是将void(A::*)()这个函数指针类型重命名为PF,这里我们需要注意一下,就是函数指针和数组指针的类型的重定义的方式和我们平时重定义普通类型时的方式是不一样的,这里大家需要注意一下(对于数组指针和函数指针而言,typedef时,新的类型名必须放在*的后面,*前必须加上类域限制)。
	//上面那句代码的意思就是将void(A::*)()这个成员函数的指针类型重定义为了PF。
	int main()
	{
	    //C++规定成员函数要加&才能成功地取到函数指针的地址,
		PF pf1=&A::func;//定义一个成员函数指针,指向A类中的func这个成员函数,接下来,我们来进行一个回调func函数的操作。
		A a;
		(a.*pf1)();//A::func();我们在进行对象调用成员函数指针时,要用到".*"这个运算符才可以。
		A*aa=new A;
		((*aa).*pf1();//A::func();A类型的指针也可以。
		return 0;
     }
  */
	cout << f9(pl, 3.333, 4.4) << endl;//77.33;
	//OK,好了,上面我们已经将补充知识讲解完了,接下来,就来简单地给大家讲述一下第53这行代码和第56这行代码中不管传值还是传指针都可以的原因:这个原因其实很简单,在function这个类模板的operator()这个成员函数中(我们这里拿f8来举例说明),我们传过去的那个Plus*类型的指针变量它并不会直接传给this指针,而是通过".*"操作符去回调相应的成员函数,我们先看第55行这句代码,我们传给function中的operator()成员函数的是一个Plus类型的对象pl,那么operator()成员函数则是去用一个Plus*类型的指针去接收(我们假设接收的那个指针为ps),在operator()函数的内部,会通过((*ps).* )(1.11,1.2)去调用相对应的那个成员函数(中间空的那部分其实就是f8对象中所存储的那个指向plusd这个成员函数的成员函数指针),以上是传指针情况的解析,接下来,我们就来看一下传值的这种情况(我们假设传给f9对象中的operator()这个成员函数的实参是pl这个对象,operator()函数中负责接收的是Plus类型的ps对象),对于传值这种情况的话,在operator()成员函数的内部,会通过(ps.* )(3.333,4.4)去调用相对应的那个成员函数(中间空的那部分其实就是f9对象中所存储的那个指向plusd这个成员函数·的成员函数指针)。好,讲到这里的话,这个原理我们就讲的差不多了,至于我们是=具体传的到底是指针还是值,编译器在这里会自己判断,从而用适合的方法去调用相对应的函数。
	function<double(Plus&&, double, double)> f10 = &Plus::plusd;
	cout << f10(Plus(), 5.555, 6.6) << endl;//12.155;传右值过去也是可以的。
	function<double(Plus&, double, double)> f11 = &Plus::plusd;
	cout << f11(pl, 7.777, 8.8) << endl;//16.577;传左值也是完全没有问题的。
	return 0;
}

  8.2 bind

template<class Fn,class ...Args>
bind(Fn&& fn, Args...args);

template<class Ret, class Fn, class ...Args>
bind(Fn&& fn, Args...args);

       1>.bind是一个函数模板,它也是一个可调用对象的包装器,我们可以把它看成是一个函数适配器,对接收的fn可调用对象进行处理后会返回一个可调用对象。bind可以用来调整参数个数和参数的顺序。bind也在<functional>这个头文件中。

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

       3>.arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数本质上都是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数位置。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为newCallable的第二个参数,以此类推。_1 / _2 / _3.......这些占位符全部都存放在一个名为placeholders的命名空间中。

#include<functional>
#include<typeinfo>
using namespace placeholders;//我们后面会使用placeholders这个命名空间中的占位符,为了方便,我们这里将这个命名空间全部展开。
int Sub(int a, int b)
{
	return a - b;
}
int SubX(int a, int b, int c)
{
	return (a - b - c) * 10;
}
int main()
{
	auto sub1 = bind(Sub, _1, _2);
	cout << sub1(10, 5) << endl;//5;我们这里借助我们前面所学的知识来梳理一下具体的调用过程,sub1就是bind函数返回的那个可调用对象,在bind函数中,bind函数将sub这个可调用对象的参数处理成了(_1,_2)这个参数,10这个实参传给了_1,5这个参数传给了_2,进而调用的sub这个函数的参数顺序就为(10,5),从而会输出5。
	auto sub2 = bind(Sub, _2, _1);
	cout << sub2(10, 5) << endl;//-5;当有的同学看到这个输出结果时,会感到很是疑惑,为什么呢?这里之所以会输出-5这个结果,究其根本原因,主要还是占位符的问题,通过对前一句代码的分析,我们知道了在bind函数中将这个可调用对象的参数给处理成了(_2,_1)这个参数,我们在当前这一句代码中sub2的两个参数分别是10和5,通过前面对_1和_2占位符的介绍可知_1为sub2的第一个参数,_2为sub2的第二个参数,因此10这个参数会传给_2,而5这个参数会传给_1,这样的话,bind函数中就调用的是参数顺序为(5,10)的Sub函数,从而才会输出结果为-5。
	//上述的操作说明bind函数可以调整参数的顺序,但是调整参数的顺序对我们来说意义不大,但是bind函数除了可以调整参数的意义之外,它还可以调整参数的个数(这个是我们比较常用的)。
	auto sub3 = bind(Sub, 100, _1);//绑死了第一参数始终是100。
	cout << sub3(5) << endl;//95;//5这个参数会传给_1,bind函数中调用的是参数为(100,5)的Sub函数,从而输出结果为95。
	auto sub4 = bind(Sub, _2, 100);//绑死了第二参数始终是100。
	cout << sub4(10, 20) << endl;//80;OK,对于bind函数来说,实际需要的参数的个数和实际传参的个数是不用相同的,但是实际传参的个数一定要比占位符的那个整数大于等于(有占位符的情况下),或者实际传参的个数至少要等于实际需要的参数个数(没有占位符的情况下)。
	//分别绑死第1、2、3个参数。
	auto sub5 = bind(SubX, 100, _1, _2);//绑死第一个参数。
	auto sub6 = bind(SubX, _1, 100, _2);//绑死第二个参数。
	auto sub7 = bind(SubX, _1, _2, 100);//绑死第三个参数。
	function<double(double, double)> f1 = bind(&Plus::plusd, Plus(), _1, _2);
	//这里是将Plus类型的对象直接绑死了,这样的话,我们就不需要每次都去传递一个Plus类型的对象了。
	//有的同学在这里就会问了,既然bind函数是对接收的fn这个可调用对象进行处理后从而会返回一个可调用对象,那么它返回的那个可调用对象和fn是不是同类型呢?对于这个问题,我们可以自己来测试一下:
	cout << (typeid(sub1) == typeid(Sub) ? "same" : "different") << endl;//different;通过打印的结果来看的话,不是同一个类型,typeid可以求出一个变量的具体类型。
	return 0;
}

9 const限定符

  9.1 顶层const和底层const

       1>.指针本身就是一个对象,它又可以指向另一个对象,因此指针涉及到本身是不是被const修饰的和那个对象是不是被const修饰的问题,C++为了能更好地区分它们,把本身被const修饰叫做顶层const,把指向的对象被const修饰叫做底层const。

       2>.大多数对象被const修饰都叫做顶层const,指针被const修饰时,*左边的const叫做底层const,*右边的const叫做顶层const。

       3>.const修饰引用时,这个const是底层const。

int main()
{
	int i = 0;
	int* const p1 = &i;//const修饰的是p1这个指针本身,因此这个const是顶层const。
	int const* p2 = &i;//const修饰的是p2这个指针指向的那个对象,因此这个const是底层const。
	int const a = 10;//const修饰的是a这个对象本身,因此这个const是顶层const。
	const int& r = a;//const修饰的是引用,因此这个const是底层const。
	return 0;
}

  9.2 constexpr

       1>.常量表达式是指指不会改变并且在编译过程中就能得到的计算结果的表达式、字面值,常量表达式初始化的那个const对象也是常量表达式,要注意变量初始化的那个const对象不是常量表达式。

       2>.C++11中引入了constexpr,constexpr可以修饰变量,constexpr修饰的变量一定是常量表达式,且必须用常量表达式对其进行初始化操作,否则的话会报错。

       3>.constexpr可以修改指针,constexpr修饰的指针是顶层const,也就是指针本身。

       4>.constexpr还可以修饰函数的返回值,要求函数体中,包含一条return返回语句,修饰的函数可以有一些其它语句,但是这些语句运行时可以不执行任何操作就可以(这句话我们看上去会感觉到有些别扭,我们这里来具体说明一下,这句话的意思其实就是说这些语句在编译时其实就已经被计算好并且被完全优化掉了,既然如此,它们在程序执行时是不会产生任何效果的),如类型名、空语句、using声明等。并且要求参数和返回值都是字面值类型(整形、浮点数、指针、引用),并且返回值必须时常量表达式。

       5>.constexpr不能修饰自定义类型,但是我们如果用constexpr修饰了类的构造函数之后就可以用constexpr去修饰相应的自定义类型了,但是既然如此的话,那么我们在初始化该构造函数成员时必须使用常量表达式对其进行初始化,并且函数体内部的语句运行时可以不执行任何操作就可以,跟修饰函数类似。

       6>.constexpr可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否是否符合常量表达式函数的要求也是不确定的。C++11标准规定,如果constexpr修饰的模板函数实例化后的结果不满足常量表达式的要求,则constexpr会被自动忽略,即该函数就等同于一个普通函数。

int main()
{
	const int a = 1;//1属于是字面值,这句代码的解释是用一个字面值给a初始化,再加上a是一个const对象,因此a它是一个常量表达式。
	const int b = a + 1;//a+1是一个常量表达式,且b是一个const对象,因此b他是一个常量表达式。
	int c = 1;//1是一个字面值,由于c不是const对象,因此c不是常量表达式。
	const int d = c;//d不是常量表达式,因为c是在运行时被赋值的,并不是在编译时就被赋值为1的。
	int arr[a];//常量表达式可以做为数组的对象,VS不支持变长数组,所以这里数组的大小必须要是编译时确认的(a是一个变量表达式,变量表达式是可以在编译阶段确定其具体的数值的)。
	constexpr int aa = 1;//1是一个字面值,aa这个变量是被constexpr修饰的,因此aa这个变量它是一个常量表达式,因此必须用常量表达式初始化。
	//constexpr int e = c;//编译器在这里会报错,e是被constexpr修饰的,因此必须要用常量表达式对e进行初始化操作,而c是在运行时被赋值为1的,在编译阶段c就还是一个变量,不是常量表达式,因此这里才会报错。
	//constexpr int* p1 = &d;//编译器会报错,报错的原因是因为权限被放大了,说到这里,哪有的同学就有疑问了,报错原因为什么是权限放大而不是未用常量表达式对其进行初始化操作呢?"&d"这一操作其实就是取出d这个变量的地址(对于这个地址我妈妈大家需要注意一下,某一块空间的地址,它其实本质上是一个值,"0x00010fg9"这是一个地址,它其实是一个用八进制的方式表示的值,只不过这个值是被用做去表示地址了而已),而地址它本质上其实就是一个字面值,也就相当于是一个常量表达式,因此不会报这个错误,而d是被const修饰过的,只能读取,不能修改,而*p1并没有被const修饰,因此*p1它既可以读取,也可以修改,因此这一句代码有权限放大的风险,因此会报错。
	//constexpr const int* p1 = &d;//加上const修饰就可以了,编译器就不会报错了。(这里给大家暂时说一下,就是我们这行代码以及上一行代码在理论上来说是可以的,但是我在实际的运行过程中是运行不出来的,我这里暂时还不知道问题处在哪里,这里大家需要注意一下)。
	return 0;
}
constexpr int size()
{
	return 10;
}
constexpr int func(int x)
{
	return 10 + x;
}
constexpr int fxx(int x)
{
	int i = x;
	++i;
	return 20 + x;
}
int main()
{
	constexpr int n1 = size();//size()函数返回的是10,而10是一个字面值,n1是一个常量表达式。
	int arr1[n1];//很多人在看到这句代码的第一眼就会下意识地想到这是一个C99变长数组,如果我们大家仔细去看的话,就会发现arr1并不是一个C99变长数组,而是一个普通地数组,通过第17行这句代码我们可知,n1是一个常量表达式,并不是一个变量,通过去,1对常量表达式地解释可知常量表达式在编译阶段是可以确定出它具体的值的,回到我们这句代码上,这句代码在编译时会变成"int arr1[10];"这一句代码,n1在编译阶段就会被替代成10,因此arr1时普通的数组,而不是C99变长数组。
	constexpr int n2 = func(10);//当func传10的时候,func返回的时常量表达式,n2是常量表达式。
	int i = 10;
	//constexpr int n3 = func(i);//编译器在这里会报错;为什么?因为func传过去的i是一个变量,不是常量表达式,既然不是常量表达式,那么在编译阶段编译器是无法确定i这个变量的具体数值的,这就导致在编译阶段无法确定func函数的值,返回值并不是常量表达式,而且n3是被constexpr修饰的,要求初始化的那个值必须是常量表达式,因此这里才会报func返回会的不是常量表达式的错误。
	int n4 = func(i);//编译器没有报错;为什么呢?在解答这个问题之前,我们大家再来补充一个知识:被constexpr修饰的变量必须用编译时就被确定的值去初始化,而被constexpr修饰的函数则可以再编译时或运行时被调用。而这句代码再运行时去调用func(i)的,而且我们如果是在运行时调用func函数的话,那么修饰func函数的那个constexpr关键字就相当于不会其作用。因此这里才不会报错。
	//既然如此的话,我们这里再来给大家说一下第21行这几句代码在调用func函数时为什么是编译时调用,而非是运行时调用(我们要知道调用函数这个操作一般来说都是在运行时才去调用相应的那个函数的),n3是被constexpr修饰的,因此n3是一个常量表达式,从而就要求必须用编译时就能确定的值去初始化,因此这里会在编译阶段去调用func函数,但是由于我们传过去的参数i是一个变量,从而导致返回的那个值不是常量表达式,进而报错,而n4没有被constexpr修饰,因此不要求编译时得到结果,那么就运行时得到结果。
	//constexpr修饰的函数默认也是被inline修饰的。
	return 0;
}
class Date
{
public:
	constexpr Date(int year)
		:_year(year)
	{ }
	constexpr int GetYear()const
	{
		return _year;
	}
private:
	int _year;
};
template<class T>
constexpr T func(T t)
{
	return t;
}
int main()
{
	constexpr Date d(2024);//对d进行初始化操作时必须用常量表达式对其进行初始化操作。
	constexpr int y = d.GetYear();//y时被constexpr修饰的,因此y是一个常量表达式,从而就要求必须用编译时就能确定的值去进行初始化,因此编译器在编译阶段就会去调用GetYear函数,我们在创建d这个对象时,是用常量表达式对_year这个成员变量进行初始化的,因此_year是可以在编译时得到结果的,因此,编译时去调用GetYear这个函数返回的那个值是一个常量表达式,符合要求,因此这里并不会报错。
	string ret1 = func("111111");//string类类型的对象不属于字面值类型,因此func函数被实例化之后不满足常量表达式的要求,且func函数是在运行时被调用的。
	constexpr int ret2 = func(10);//整形类型属于字面值类型,func函数被实例化之后满足常量表达式的要求,ret2是被constexpr修饰的,因此ret2是一个常量表达式,因此是在编译阶段去调用func函数的,我们传过去的是一个常量表达式,因此func函数的返回值是可以在编译时得到具体的值的,因此不会报错。
	return 0;
}

10 处理类型

  10.1 auto

       1>.前面的课程中我们已经用过auto,auto是一个类型说明符,他让编译器替我们去分析表达式的类型,如:auto x = y + z ;编译器这里会自动根据y+z相加的结果来推导x的具体类型,在一些类型比较长的场景中,比如说迭代器的类型用auto来代替就非常好用。

       2>.编译器在推导auto类型时,有时候也会和初始值得类型不一样,编译器会适当的去改变结果的类型,使其更加符合初始化的规则。首先我们在使用引用类型初始化时,真正参与初始化的其实是引用对象的值,所以编译器推导出auto为引用对象的类型,而不是引用。其次是一个带有const属性的值初始化auto对象推导出会忽略掉顶层const,而保留底层const。

       3>.如果想使用auto推导出顶层const,需要我们明确的指出:const auto x = ci; 。

       4>.auto不能自动推导出引用类型,所以我们如果想要将auto推导为引用类型的话,则需要我们明确指出。并且带有const属性的值初始化auto对象,推导时忽略掉顶层const,保留底层const的规则仍然适用。

int main()
{
	int i = 0;
	const int ci = 42;//顶层const。
	int* const p1 = &i;//顶层const。
	const int* p2 = &ci;//底层const。
	const int& ri1 = ci;//底层const。
	const int& ri2 = i;//底层const。
	int& ri3 = i;
	auto r1 = ci;//编译器在这里推导出r1的类型为int,ci有const属性,编译器在这里推导auto时会忽略掉顶层const,因此编译器这里推导出auto为int。这一步是拷贝操作。
	r1++;//编译器在这里不会报错。
	auto r2 = p1;//推导出r2的类型为int*类型,编译器在这里推导auto时会忽略掉顶层const,因此推导出的auto就为int*类型了。
	r2++;//编译器不会报错,r2++之后,是r2这个指针指向了下一个空间,而p1还指向的是原来的那个空间,指向不变。
	auto r3 = p2;//推导出r3的类型为const int*类型,编译器在这里只会忽略顶层const,对于底层const则是保留不变。
	//(*r3)++;//编译器在这里会报错,底层const修饰的是r3指向的那个对象的值,也就是*r3,不可改变。
	auto r4 = ri1;//推导出r4的类型为int类型,因为编译器在用引用对象初始化auto类型的对象时,是按照引用对象所引用的那个值进行初始化的,说白了,这里其实就是用ci进行初始化的,因此编译器这里是根据ci的类型来推导r4的类型为int。
	r4++;//既然是int类型的话,那么上一句代码就相当于是拷贝操作,因此r4可以++。
	const auto r7 = ci;//推导出r7的类型为const int类型,按照前面的推导规律来说的话,auto推出为int,前面再加上一个const修饰,因此r7的类型就是const int类型,r7不可修改。
	auto& r8 = ri1;//推导出r8的类型为const int&类型,因为我们在auto后面多加了一个&,就相当于告诉编译器r8是对ri1的一个引用,通过我们前面所说的推导规则,推导出auto是int,但是由于我们在auto的前面加上了一个&操作符,编译器在这里会认为r8是对ri1变量的引用,而ri1是被const修饰的,因此,权限会被放大。r8不可修改。
	return 0;
}

  10.2 decltype

       1>.如果我们希望用表达式推出变量的类型,但是不想用表达式的值去初始化变量,那么这时我们就可以使用decltype。decltype(f()) x;需要注意的是编译器并不会实际调用f函数,而是用f的返回值类型来作为x的类型。

       2>.decltype处理const和引用的方式和auto也有所不同,decltype(const变量表达式) x;x的类型推导为const T,decltype会保留顶层const。decltype(引用变量表达式) x;x的类型推导为T&,decltype会保留引用,要注意这里跟auto是完全不同的。

       3>.decltype还有一些特殊处理比较奇怪,decltype(*p) x;x的类型推导出来是T&,decltype推导解引用表达式时,推导出的类型是引用。decltype((i)) x;x的类型是T&,decltype推导括号括起来的左值表达式时,推导出类型是引用。

int main()
{
	int i = 0;
	const int ci = 0;
	const int& rci = ci;
	decltype(i) m = 1;//decltype会用i变量的类型来作为m的类型,i的类型为int类型,因此m就为int类型。
	m++;//可以修改。
	decltype(ci) n = 1;//decltype会保留顶层const,因此n的类型就会被推导为const int。
	//n++;//不可修改。
	decltype(rci) x = i;//decltype会保留引用,因此x的类型就会被推导为const int&。
	//x++;//不可修改。
	i++;
	cout << x << endl;//1;说明x的类型确实为引用。
	//decltype(rci) z;//编译器会报错,z的类型会被推导为const int&,也就是说,z是一个引用变量,通过我们前面的解析可知,引用变量必须要解析初始化操作,否则不会报错。
	int* p = &i;
	decltype(*p) p1 = i;//通过我们前面的学习可知,decltype在推导解引用表达式时,推导出的类型是引用,因此p1的类型被推导出来是int&。
	p1++;
	cout << i << endl;//2;通过输出结果来看的话,p1的类型确实是引用。
	decltype((i)) y = i;//通过我没钱买的学习,我们可以得知decltype在推导括号括起来的左值表达式时,推导出来的类型是引用,因此y的类型是int&。
	y++;
	cout << i << endl;//3;
	return 0;
}

       4>.auto必须要通过初始化值推导类型,像类的成员变量这种就没办法使用auto,decltype则可以很好的解决这样的问题。

template<class T>
class A
{
public:
	void func(T& container)
	{
		_it = container.begin();
	}
private:
	//我们写一个迭代器类型的成员变量,既然要写一个迭代器类型的成员变量,那么我们首先会面临一个问题,就是这个迭代器的类型我们是不确定的,有可能是iterator,也有可能是const_iterator类型,我们这里不能使用auto,为了方便且准确地确定这个迭代器的类型,我们这里用decltype这个关键字。
	decltype(T().begin()) _it;
};
int main()
{
	const vector<int> v1;
	A<const vector<int>> obj1;//传给A这个模板类之后,T被识别为是const vector<int>类型,这样的话,第11行那句代码就相当于是decltype(const vector<int>().begin()) _it;这整个匿名对象都是被const修饰的,在去调用begin()这个函数时,调用最匹配的,也就被const修饰的那个begin函数,返回值为const_iterator类型的begin函数,因此_it的类型会被推导为是const_iterator类型。
	vector<int> v2;
	A<vector<int>> obj2;//T被识别为是vector<int>类型,这样的话,第11行那句代码就相当于是decltype(vector<int>().begin()) _it;调用的那个begin函数是最匹配的那个函数,因此调用的是不被const修饰的那个begin函数,返回值为iterator类型的begin函数,因此_it的类型会被推导为是iterator类型。
	return 0;
}

       5>.decltype还可以用来解决函数尾值返回类型的问题,又是一个函数模板的类型是不确定的,是跟某个参数对象有关的,需要进行指导,直接用decltype推导去做返回值类型是不行了,因为C++是前置语法,往前是找不到这个推导的对象的,这个对象在参数里面,从所以auto做返回值,然后->decltype(对象)做尾置推导。

//我们写一个函数模板。
template<class Iter>
//decltype(*it1) Func(Iter it1,Iter it2)//这样写是错误的,编译器在这里会报错,我们不可以直接用decltype去推导Func函数的返回值,因为C++是前置语法,在读取代码时时从上往下去读取的,当它读取到这句代码时,它首先读到"*it1",读到这里时,编译器它不认识"it1"是啥,编译器会停止继续往后读取,去到前面去找it1,没有找到,因此编译器在这里会报错。
auto Func(Iter it1, Iter it2) -> decltype(*it1)
{
	auto& x = *it1;
	return x;
}
int main()
{
	vector<int> v = { 1,2,3 };
	auto ret1 = Func(v.begin(), v.end());//将参数传给Func函数之后,Iter被推到成vector::iterator类型,it1是一个迭代器(vector::iterator)的变量,因此decltype(*it1)这个操作会推导出Func函数返回的是一个int类型的变量,说到这里,想必就一定会有同学在这里询问,前面不是说decltype在推导解引用表达式时,推导出的类型是T&吗?为什么这里推导的是*it1(it1迭代器指向的那个位置中存储的值),而不是it1本身呢?说到这个问题,这里我们首先要注意一个事情,就是前面说的情况是推导解引用时出现的特殊情况会那样处理,而大部分情况则是会像我们这里的这样,先对其继续解引用操作,然后去推导解引用后的值的类型,因此这里才会推导出Func函数的返回值是一个int类型的变量。
	auto ret2 = Func<decltype((v.begin()))> (v.begin(), v.end());//这里就是我们前面所说的那个特殊情况,decltype<v.begin()>这个就被推到成vector::iterator&类型,导致Iter就为那个类型了。
	return 0;
}

  10.3 typedef和using

       1>.C++98中我们一般使用typedef重定义类型名,也很方便,但是typedef不支持带模板参数的类型重定义。C++11中新增了using可以替代typedef,using的别名语法覆盖了typedef的全部功能,还支持带模板参数重定义的语法。

       2>.using类型别名=类型;。

using namespace std;
using CountMap = map<string, string>;
using Iter = typename map<string, string>::iterator;
template<class T>
using MapIter = map<int, T>;
int main()
{
	CountMap m1;//创建了一个类型为map<string,int>类型的map对象m1。
	MapIter<int> m2;//创建了一个类型为map<int,int>类型的map对象m2。
	Iter il = m1.begin();//创建了一个类型为map<string,string>::iterator类型的迭代器,用m1对象的begin()对其继续初始化操作。
	return 0;
}

       OK,今天我们就先讲到这里了,那么,我们下一篇再见,谢谢大家的支持! 

评论 109
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值