C++11中的特性

news2024/10/6 0:08:37

        这里主要讲解一些C++11相较于C++98所新增的比较实用的新特性。

        C++11的官方文档:C++11 - cppreference.comicon-default.png?t=O83Ahttps://en.cppreference.com/w/cpp/11

一、列表初始化(List-initialization)

(一)、使用“{}”进行初始化

        在C++98中,可使用“{}”对数组或结构类型的变量进行初始化:

#include <iostream>
using namespace std;

//表示点的坐标的结构体
struct Point
{
	int x;
	int y;
};

int main()
{
	int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
	int arr2[10] = { 0 };
	Point p = { 10,100 };

	cout << "arr1:";
	for (int i = 0; i < sizeof(arr1) / sizeof(arr1[0]); i++)	//遍历并输出arr1
	{
		cout << arr1[i] << " ";
	}
	cout << endl;

	cout << "arr2:";
	for (int i = 0; i < sizeof(arr2) / sizeof(arr2[0]); i++)	//遍历并输出arr2
	{
		cout << arr2[i] << " ";
	}
	cout << endl;

	cout << "p:";
	cout << "x = " << p.x << " y = " << p.y << endl;

	return 0;
}

运行结果: 

arr1:1 2 3 4 5 6 7 8 9 10
arr2:0 0 0 0 0 0 0 0 0 0
p:x = 10 y = 100
 

        C++11扩大了用大括号“{}”括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,并且使用初始化列表初始化时,等号“=”可以省略不写。

#include <iostream>
using namespace std;

//表示点的坐标的结构体
struct Point
{
	int x;
	int y;
};

//表示人的基本信息的类
class Person
{
private:
	string _name;	//姓名	
	int _age;	//年龄
	string _addr;	//住址
public:
	//构造函数
	Person(const string& name, int age, const string& addr):
		_name(name),_age(age),_addr(addr)
	{}

	//打印基本信息
	void showInfo()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "address:" << _addr << endl;
	}
};

int main()
{
	//---------------------------------------------------------------------------------------------
	int a1 = 5;	//普通的初始化
	int a2 = { 6 };	//使用列表初始化对内置类型进行初始化
	int a3{ 7 };	//省略等号“=”
	cout << a1 << " " << a2 << " " << a3 << endl;

	//---------------------------------------------------------------------------------------------
	//相较于C++98,省略了等号:
	int arr1[]{ 1,2,3,4,5,6,7,8,9,10 };
	int arr2[10]{ 0 };
	Point p{ 10,100 };

	cout << "arr1:";
	for (int i = 0; i < sizeof(arr1) / sizeof(arr1[0]); i++)	//遍历并输出arr1
	{
		cout << arr1[i] << " ";
	}
	cout << endl;

	cout << "arr2:";
	for (int i = 0; i < sizeof(arr2) / sizeof(arr2[0]); i++)	//遍历并输出arr2
	{
		cout << arr2[i] << " ";
	}
	cout << endl;

	//输出点p坐标
	cout << "p:";
	cout << "x = " << p.x << " y = " << p.y << endl;

	//---------------------------------------------------------------------------------------------
	//C++11的列表初始化也适用于new表达式
	int* pArr = new int[4] {1, 2, 3, 4};

	cout << "pArr:";
	for (int i = 0; i < 4; i++)	//遍历pArr
	{
		cout << pArr[i] << " ";
	}
	cout << endl;
	
	//----------------------------------------------------------------------------------------------
	//创建对象时使用列表初始化的方式调用构造函数进行初始化
	Person man1 = { "Maxine Caulfield",18,"Arcadia Bay" };
	Person man2{ "Chloe Price",19,"Arcadia Bay" };
	cout << "man1:" << endl;
	man1.showInfo();
	cout << endl;
	cout << "man2:" << endl;
	man2.showInfo();

	delete[] pArr;
	return 0;
}

        运行结果: 

5 6 7
arr1:1 2 3 4 5 6 7 8 9 10
arr2:0 0 0 0 0 0 0 0 0 0
p:x = 10 y = 100
pArr:1 2 3 4
man1:
name:Maxine Caulfield
age:18
address:Arcadia Bay

man2:
name:Chloe Price
age:19
address:Arcadia Bay
 

        需要注意一点:在使用列表初始化时,如果发生了类型数据截断,则会警告或者报错的。 

int main()
{
	const double PI = 3.1415926535897;

	//浮点型赋值给整形发生数据截断
	int a = PI;	//初始化的结果为 a = 3
	int b = { PI };	//编译不通过

	return 0;
}

        报错: 

 

         解决方法:强制类型转换一下即可。

int b = { (int)PI };

(二) initializer_list

        initializer_listC++11提供的新类型,可以通过迭代器访问initializer_list中的值,该列表是某类型的元素列表,每个元素都是常量,不能够被修改

        使用例子:

#include <iostream>
#include <initializer_list>
#include <vector>
#include <map>

using namespace std;

int main()
{
	initializer_list<int> list1 = { 1,2,3,4,5,6,7,8,9,10 };	

	//遍历
	initializer_list<int>::const_iterator it = list1.begin();
	while (it != list1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	
	//------------------------------------------------------------------
	//使用场景,使用initializer_list里的多个元素对容器进行初始化
	vector<int> v1(list1);						//调用了vector里的vector(initializer_list<int>)
	vector<int> v2({ 1,2,3,4,5,6,7,8,9,10 });	//{ 1,2,3,4,5,6,7,8,9,10 }相当于list1
	vector<int> v3 = { 1,2,3,4,5,6,7,8,9,10 };	//调用了vector里的operator(initializer_list<int>)
	cout << "v1:";
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	
	cout << "v2:";
	for (auto& e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	
	cout << "v3:";
	for (auto& e : v3)
	{
		cout << e << " ";
	}
	cout << endl;

	//initializer_list搭配多参数构造函数的隐式类型转换
	map<string, int> m({ {"abandon",1},{"baby",2},{"cab",3} });
	//{"abandon",1},{"baby",2},{"cab",3} 分别调用了 pair<string,int> 的构造函数构造了三个pair对象
	for (auto& e : m)
	{
		cout << e.first << " " << e.second << endl;
	}
	return 0;
}

        运行结果: 

1 2 3 4 5 6 7 8 9 10
v1:1 2 3 4 5 6 7 8 9 10
v2:1 2 3 4 5 6 7 8 9 10
v3:1 2 3 4 5 6 7 8 9 10
abandon 1
baby 2
cab 3
 

二、声明 

(一)、auto关键字

        C++11中废弃auto原来在C++98的用法(表明某变量是局部自动存储类型),C++11将其用于实现自动类型推断。auto要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。需要注意,auto不能用于推导函数参数的类型,因为函数在编译期间还不知道函数传递的参数是什么,所以无法推导出对应的类型替换auto

        使用例子:

#include <iostream>
#include <initializer_list>
#include <vector>
#include <map>
using namespace std;

//随便写的一个函数
//auto可以作为函数返回值推导返回值的类型,这里的auto根据返回值a的类型推导出int类型
//将auto替换int
auto func(int a, double b, char c)	
{
	cout << a << b << c << endl;
	return a;
}

int main()
{
	int a;
	double b;
	char c;

	//识别并打印a,b,c的类型
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;

	//使用auto推导函数func的类型
	auto pFunc = func;
	cout << typeid(pFunc).name() << endl;
		
	//使用auto推导迭代器类型
	map<string, string> dict = { {"abandon", "放弃"}, {"baby", "婴儿"} };
	map<string, string>::iterator it1 = dict.begin();	
	auto it2 = dict.begin();	

	//判断it1和it2的类型和内容是否相等
	if (it1 == it2)
		cout << "yes" << endl;

	return 0;
}

        运行结果: 

int
double
char
int (__cdecl*)(int,double,char)
yes

        当我们要声明某个变量,该变量的类型名比较长,并且在语境中我们清楚知道该类型是什么时,就用auto代替具体的类型使代码简洁,让编译器帮我们推导具体的类型。 

(二)、decltype关键字        

        关键字decltype将变量的类型声明为表达式指定的类型。相较于typeid,typeid只能识别某变量的类型是什么,但不能用来声明该类型的变量。

        当我们得到了某匿名对象,即不知道该对象的类型是什么,但是想要再声明一个和该匿名对象一样类型的对象时,decltype的用处就来了。

        使用用例:

#include <iostream>
using namespace std;

//定义一个匿名对象,随手创建一个man1对象
struct //省略了Person
{
	string _name;
	int _age;
	string _addr;

	void showInfo()
	{
		cout << "这是一个匿名对象" << endl;
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "addr:" << _addr << endl;
 	}
}man1;

int main()
{
	//初始化man1对象
	man1._name = "Maxine Caulfield"; man1._age = 18; man1._addr = "Arcadia Bay";
	man1.showInfo();

	cout << endl;
	//使用man1推导其类型并声明一个man2对象
	decltype(man1) man2;
	man2._name = "Chloe Price"; man2._age = 19; man2._addr = "Arcadia Bay";
	man2.showInfo();

	return 0;
}

        运行结果: 

这是一个匿名对象
name:Maxine Caulfield
age:18
addr:Arcadia Bay

这是一个匿名对象
name:Chloe Price
age:19
addr:Arcadia Bay
 

        decltype()里头可以填表达式(如 x+y之类的),声明出的变量或对象类型为表达式计算结果的类型(如表达式为 1 * 1.0 的话就会推导出double)。 

 (三)、nullptr关键字

        C++中NULL被定义成字面量0这样会带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。所以,我们平常应该更习惯的使用nullptr而不是NULL来表示空指针

        例子:

#include <iostream>
using namespace std;

//下面的两个func2构成了函数重载
void func2(int a)	//参数为整形的版本
{
	cout << "void func2(int a)" << endl;
}

void func2(int* p)	//参数为指针的版本
{
	cout << "void func2(int* p)" << endl;
}

int main()
{
	cout << NULL << endl;
	cout << (void*)NULL << endl;

	int* p1 = NULL;	//旧的写法,相当于 int* p1 = 0;
	int* p2 = nullptr;	//推荐使用新的写法来定义空指针

	cout << endl;
	//当我们想要调用指针版本的func2函数
	func2(NULL);	//由于NULL被宏定义为0,所以调用到整形版本的func2(int)函数去了。
	func2((int*)NULL);
	func2(nullptr);
	
	return 0;
}

运行结果: 

0
0000000000000000

void func2(int a)
void func2(int* p)
void func2(int* p)

 

三、范围for (range-based for loop)

        范围for用于对某对象的遍历,前提是需要该对象支持迭代器,并且能够知道迭代的范围。还需注意的是从对象中取数据的两种方式(值拷贝和引用)会影响效率。

        示例:

int main()
{
	//-------------------------------------------------------------------------------------
	int a[] = { 1,2,3,4,5 };
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;

	//范围for不能用于遍历动态开辟出来的数组,如下面这个p所指向的数组空间
	int* p = new int[5] {6, 7, 8, 9, 10};
	
	//-------------------------------------------------------------------------------------
	vector<int> v = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto e : v)	//值拷贝方式遍历v
	{
		cout << e << " ";
	}
	cout << endl;

	//--------------------------------------------------------------------------------------
	map<string, string> dict = { {"abandon", "放弃"}, {"baby", "婴儿"} ,{"cab","出租车"}};
	for (const auto& e : dict)	//const引用方式遍历dict
	{
		cout << e.first << "->" << e.second << endl;
	}

	cout << endl;
	for (auto& e : dict)	//非const引用方式遍历dict
	{
		e.second += "。";
		cout << e.first << "->" << e.second << endl;
	}
	return 0;
}

运行结果: 

1 2 3 4 5
1 2 3 4 5 6 7 8 9 10
abandon->放弃
baby->婴儿
cab->出租车

abandon->放弃。
baby->婴儿。
cab->出租车。
 

  四、STL中的变化

(一)、新增的几个容器

Containers - C++ Reference (cplusplus.com)icon-default.png?t=O83Ahttps://legacy.cplusplus.com/reference/stl/

         C++11新增了以上的几个容器,分别是array(定长数组)、forward_list(单向链表)、unordered_map、unordered_multimap、unordered_set和unordered_multiset。

        其中unordered代表无序的意思,无序的set、map底层使用的是哈希映射的方式存储的值,搜索效率比普通的set、map要高,搜索的时间复杂度可以达到常数级别。

(二)、容器内的新方法

        在C++11中,容器都增加了新的方法,尤其是支持移动语义的接口(如参数为右值引用push_back、万能引用+参数包的emplace_back、右值引用的insert、万能引用+参数包的emplace等接口)更值得注意。这些新增的接口与老接口相比,在使用方法上和操作结果上似乎没什么区别,但是它们的底层实现会是天差地比,这里先放一放,在后面会通过介绍具体例子讲解这些接口底层和作用。

push_back

emplace_back

insert

emplace

五、右值引用和移动语义 

(一)、右值引用的概念

        左值引用(&)就是对左值的引用,右值引用(&&)就是对右值的引用。左值和右值可以通过是否能够取得其值(设该值为x)的地址来区分,如果能够获得到x的地址,那么x为左值,否则为右值,并且不能给右值赋值。左值引用和右值引用的本质都是给x取别名。

        下面通过例子看看哪些是右值:

int add(int x,int y)
{
	return x + y;
}

int main()
{
	int x = 1, y = 2;

	//右值(无法获得其值的地址),更不能赋值
	10;			//&10 和 10 = x	是不允许的
	(x + y);	//&(x+y) 和 x + y = 10	是不允许的
	add(x, y);	//&add(x, y)	add(x, y) = 10	是不允许的
	
	//对右值的引用
	int&& rR1 = 10;
	int&& rR2 = (x + y);
	int&& rR3 = add(x , y);
	return 0;
}

        我们发现右值都是一些临时变量,当右值所在的这一行结束后,临时变量的生命周期就到头了,这些临时变量或临时对象叫做将亡值

        下面我们来看一下左值引用和右值引用有哪些关系:

int add(int x,int y)
{
	return x + y;
}

int main()
{
	//---------------------------------------------------------------------------------------
	//非const修饰的左值引用只能修饰左值
	int a = 1;
	int& leftRef1 = a;	//取a的别名 leftRef1
	//试图对add的返回值取别名
	//int& leftRef2 = add(1, 2);		//编译报错,add的返回值不是左值

	//const修饰的左值引用可以引用左值也能够引用右值
	const int& leftRef3 = a;
	const int& leftRef4 = add(1, 2);
	
	//---------------------------------------------------------------------------------------
	//右值引用只能引用右值
	int&& rightRef1 = add(1, 2);
	//尝试对a取右值引用
	//int&& rightRef2 = a;	//编译报错,a是左值	
	
	//右值引用可以引用move后的左值
	int&& rightRef3 = move(a);
	return 0;
}

        在一定情况下,我们发现左值引用和右值引用其实都可以对左值和右值进行引用。 

        总结就是:

1、 非const修饰的左值引用只能引用左值,而const修饰的左值引用既可以引用左值也可以引用右值;

2、右值引用只能引用右值,但是右值引用可以引用move后的左值,这里move的功能可以理解为把左值转为了右值让语法层面上能够通过(因为在底层都是指针)。

(二)、右值引用的使用场景

        我们需要使用一个自己模拟实现的string来讲解,string的模拟实现代码就贴在文章的最后面,可以自行取用。总之就默认现在有一个自己模拟实现的string类定义在MyString的命名空间内。

        分析下面代码:

//自行模拟实现一个to_string函数,实现将整形转化为字符串类型
MyString::string to_string(int num)
{
	bool isNegative = false;	//默认为非负数

	if (num < 0)
	{
		isNegative = true;	//为负数
		num *= -1;	//变号
	}

	MyString::string ret;
	
	while (num != 0)
	{
		char ch = '0' + num % 10;
		num /= 10;
		ret.push_back(ch);
	}

	if (isNegative)
		ret.push_back('-');

	reverse(ret.begin(), ret.end());	//翻转字符串,头文件: <algorithm>

	return ret;
}

int main()
{
	MyString::string str = to_string(12345);
	cout << str << endl;
	return 0;
}

         在to_string的函数体内,将ret返回并将值赋值给str进行构造,进行了如下过程。

        所以在这个过程中,一共进行了两次拷贝构造(在编译器不进行任何优化的情况下)。 拷贝是有时间代价的,因为如果要拷贝的字符串很长,那么就会花费较长的时间拷贝。

        已经知道返回值是已经构造好的右值x,是一个将亡值,在过了这一行后这个将亡值就会销毁,那么右值引用的用处就来了,我们可以让str字符串的内容和这个将亡值x交换一下,也就是调用一下swap,那么就可以减少一次拷贝的开销了。这就叫做移动语义。

        下面这是string类内移动赋值的代码实现:

	string& string::operator=(string&& s)
	{
		cout << "右值引用移动赋值重载" << endl;
		swap(s);
		return *this;
	}

        代码很简单,当给某string类对象赋值时,如果赋值的对象是右值,那么就会调用对应的移动赋值函数,将双方的内容交换一下,就减少了一次拷贝的开销了。结果就是临时变量x的内容转移到了str上。

        编译器一般还会对上面的过程再进行优化,会出现下面这些情况。

        跳过过程1,也就是不构造临时对象x,直接用函数体内的ret对str进行移动赋值操作,将ret的内容转移到str上。

        加大优化力度,函数体内不就是对ret进行构造,给ret修该修改,然后将ret的值转移给str吗?这下子可厉害了,本来对ret的构造直接变成对str的构造,也就是说这个ret在函数体内被完完全全的替换成了str,函数体内对ret的操作就相当于直接在str上操作,整个过程就调用了一次str的构造函数。 

        类似的,我们来看看string类的移动构造:

	string::string(string&& s)
	{
		cout << "右值引用移动复制构造" << endl;
		swap(s);
	}

       就这么简单,把右值x的内容与将要构造的对象y的内容进行交换就行了。为了保证交换的对象y总是初始化过的,保证给string的成员变量赋初始值很重要。即在string类的下面这三行初始化的代码很重要。

private:
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;

        下面通过一个例子感受一下移动构造:

int main()
{
	MyString::string str1("str1");	//构造一个str1对象,其内容为str1
	
	cout << str1 << endl;	//输出str1的内容,此时str1的内容还在

	MyString::string str2(move(str1));	//将其str1转化为右值,这个时候就会将str1识别成右值,对str2进行一次移动构造

	//输出str1和str2的内容,发现str1的内容已经不见了,被转移到了str2上。
	//结果就是 str1的内容被置空了,str1原本的内容转移到了str2上面
	//如果str1是通过to_string传回来的临时变量x,
	// 那么就会将临时变量x的内容转移到str2上,临时对象x不能够再被访问。
	cout << str1 << endl;
	cout << str2 << endl;
	return 0;
}

运行结果: 

构造函数
str1
右值引用移动复制构造

str1
 

 (三)、移动语义

        没有移动语义之前,构造和拷贝操作都是靠const修饰的左值引用来完成的,const修饰的左值引用既可以引用左值和右值,对左值和右值一视同仁,即另外复制一段和待拷贝的左值或右值一模一样的内存空间。而在有了移动语义之后,在对右值进行拷贝时,就不需要另外开辟一段新的内容对右值进行拷贝了,我们可以直接转移右值的资源到我们所需要构造或赋值的对象上,这样可以节省拷贝的开销。

        了解了移动构造和移动赋值之后,我们就可以知道参数为右值引用的接口是用来干什么的了。比如容器中参数为右值引用的push_back和insert接口就是为了实现移动语义在使用右值插入时可以通过转移资源的方式来节省拷贝的开销。

        支持移动语义的容器接口

int main()
{
	vector<MyString::string> v;
	cout << "-----------------------" << endl;

	v.push_back(MyString::string("=-=-=-=-=-=-=-="));	//先构造一个临时对象,再将其内容转移到v[0]下。
	cout << v[0] << endl;
	v[0] = to_string(123456);	//返回一个临时对象,将其内容转移到v[0]下,原来v[0]的内容交换到了临时对象上,让临时对象帮忙进行资源释放操作
	cout << v[0] << endl;
	return 0;
}

 运行结果:

-----------------------
构造函数
右值引用移动复制构造
=-=-=-=-=-=-=-=
构造函数
右值引用移动赋值重载
123456

(四)、完美转发

1、完美转发的使用

        我们来看下面这个场景:

//构成函数重载的两个func函数
void func(const int& a)	//const修饰的左值引用
{
	cout << "void func1(const int& a)" << endl;
}

void func(int&& a)	//右值引用
{
	cout << "void func2(int&& a)" << endl;
}

void funcL(int& a)	//左值引用
{
	func(a);
}

void funcR(int&& a)	//右值引用
{
	func(a);
}

int main()
{
	int a = 10;
	funcL(a);
	funcR(move(a));
	return 0;
}

        运行结果: 

void func1(const int& a)
void func1(const int& a)

 

        我们发现,在funcL调用func,调用到的是符合我们预期的左值版本的func函数,而在funcR里调用func,调用到的也是左值版本的func函数,这不符合我们的预期,funcR里的a不是右值吗,为什么func(a)的调用会调用到左值版本的func函数。

        这是因为,我们在用右值引用给右值取别名时右值引用(别名)本身其实是左值,也就是说funcR里的a是左值(funcR里头的右值a退化成了左值a),因为是左值,所以可以对a进行修改的,在funcR里头修改a的值会影响main函数里的a的值。如果我们想要在funcR里调用func函数时调用到右值版本的func函数,则需要对a进行一次move操作。至于为什么是左值,想一下之前string里调用swap的例子,因为是左值,所以才能够调用swap转移资源。

        修改后的代码:

//构成函数重载的两个func函数
void func(const int& a)	//const修饰的左值引用
{
	cout << "void func(const int& a)" << endl;
}

void func(int&& a)	//右值引用
{
	cout << "void func(int&& a)" << endl;
}

void funcL(int& a)	//左值引用
{
	func(a);
}

void funcR(int&& a)	//右值引用
{
	func(move(a));	//这里的a仍然是左值,其引用的对象是右值,move(a)是右值
	a = 12;			//证明了a确实是个左值,因为可以修改
}

int main()
{
	int a = 10;
	cout << a << endl;
	funcL(a);
	funcR(move(a));
	cout << a << endl;
	return 0;
}

运行结果: 

10
void func(const int& a)
void func(int&& a)
12

 

        在了解了上面这个用例后,我们再来了解一下什么是完美转发。使用完美转发需要使用模板参数的万能引用 + std::forward<T>()函数完美转发。 

        来看样例:

//构成函数重载的四个func函数
void func(int& a)	//左值引用
{
	cout << "void func(int& a)" << endl;
}

void func(const int& a)	//const修饰的左值引用
{
	cout << "void func(const int& a)" << endl;
}

void func(int&& a)	//右值引用
{
	cout << "void func(int&& a)" << endl;
}

void func(const int&& a)	//const 右值引用
{
	cout << "void func(const int&& a)" << endl;
}

// 这里T&& t 并不是单指右值引用,而是代表万能引用,可以接收左值也可以接收右值。
// 万能引用可以是四种不同的引用类型:引用、const引用、右值引用、const右值引用
// 引用后仍然会退化为左值,所以函数体内的t都是左值
// 如果需要在传递t时能够保持原有的左值或右值属性,则需要使用forward<T>进行完美转发操作
template<class T>
void funcPerfect(T&& t)	
{
	func(std::forward<T>(t));	//std::forward<T>(t)是一个仿函数,可以让参数t保持原生类型传递下去
}

int main()
{
	int a = 11;
	const int b = 22;
	
	funcPerfect(a);			//传入左值
	funcPerfect(b);			//传入const修饰的左值
	funcPerfect(move(a));	//传入右值
	funcPerfect(move(b));	//传入const修饰的右值
	return 0;
}

        运行结果:

void func(int& a)
void func(const int& a)
void func(int&& a)
void func(const int&& a)

2、完美转发的使用场景

        比如我们模拟实现一个list容器(list的代码也会在后面给出),我们在实现尾插操作push_back时,可以复用insert进行尾插,在insert里构造结点。在这个过程中我们可以使用完美转发,在构造结点时通过传递的参数是左值还是右值决定构造的方式,如果是左值,则调用拷贝构造;而如果是右值,则调用移动构造。

        

        测试代码:                         

int main()
{
	MyList::list<MyString::string> ls;
	MyString::string str1("str1");
	cout << str1 << endl;

	cout << "----------------------------" << endl;
	//插入一个左值str1
	ls.push_back(str1);
	cout << "str1 =" << str1 << endl;
	cout << endl;

	//将str1转成右值再插入
	ls.push_back(move(str1));
	cout << "str1 = " << str1 << endl;
	return 0;
}

        运行结果分割线--------上面的内容不用管,是因为list构造一个哨兵位所调用的): 

构造函数
复制构造
构造函数
str1
----------------------------
复制构造
str1 =str1

右值引用移动复制构造
str1 =
 

        同理,头插push_front也是通过万能引用 + 完美转发实现上述的功能。

六、关于类的几个新功能

(一)、新增默认成员函数

        在原来的C++的类中,有6个默认的成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const 取地址重载。

        而在C++11中有了移动语义后,新增两个默认成员函数:移动构造移动赋值重载

        对于移动构造和移动赋值重载有下面几点需要注意:

1、如果一个类没有定义自己的移动构造函数、析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,编译器会为其生成一个默认的移动构造函数。

        默认移动构造函数的行为:对于内置类型(如int、double等),它会按字节直接拷贝(即所谓的“浅拷贝”)。对于类成员(包括其他类的对象),如果成员类型实现了移动构造函数,则调用该成员的移动构造函数;如果成员类型没有实现移动构造函数,但实现了拷贝构造函数,则调用拷贝构造函数;如果成员类型既没有移动构造函数也没有拷贝构造函数,则可能导致编译错误(不显式写出的话一般会默认生成一个)。

2、类似地,如果类没有定义自己的移动赋值运算符、析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,编译器会为其生成一个默认的移动赋值运算符重载函数。

        默认移动赋值运算符重载函数的行为:对于内置类型,它通常也是按字节直接拷贝(浅拷贝)。对于类成员,如果成员类型实现了移动赋值运算符,则调用该成员的移动赋值运算符;如果成员类型没有实现移动赋值运算符,但实现了拷贝赋值运算符,则调用拷贝赋值运算符;如果成员类型既没有移动赋值运算符也没有拷贝赋值运算符,则可能导致编译错误。

3、如果类中实现有移动构造和移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

        我们来验证一下:         

//喜闻乐见的Person类
class Person
{
private:
	MyString::string _name;	//自定义类型
	int _age;	//内置类型
public:
	Person(const char* name, int age) :_name(name), _age(age) {}	//构造函数
	
	//Person(const Person& p)
	//{
	//	cout << "自行实现了拷贝构造" << endl;
	//	_name = p._name;
	//	_age = p._age;
	//}

	//Person(Person&& p)
	//{
	//	cout << "自行实现了移动构造" << endl;
	//	_name.swap(p._name);
	//	_age = p._age;
	//}
	//
	//Person& operator=(const Person& p)
	//{
	//	cout << "自行实现了赋值重载" << endl;
	//	_name = p._name;
	//	_age = p._age;
	//	cout << endl;
	//}

	//Person& operator=(Person&& p)
	//{
	//	cout << "自行实现了移动赋值" << endl;
	//	_name.swap(p._name);
	//	_age = p._age;
	//	return *this;
	//}

	void showInfo()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

	//~Person()
	//{
	//	cout << "自行实现了析构函数" << endl;
	//}
};

int main()
{
	cout << "------------------     构造两个Person对象     -----------------------" << endl;
	Person man1("Maxine Caulfield", 18);
	Person man2("Chloe Price", 19);

	man1.showInfo();
	man2.showInfo();

	cout << "------------------测试编译器自动生成的移动赋值-----------------------" << endl;
	//测试编译器自动生成的移动赋值
	man1 = move(man2);

	man1.showInfo();
	man2.showInfo();

	cout << "------------------测试编译器自动生成的移动构造-----------------------" << endl;
	Person man3(move(man1));
	man1.showInfo();
	man3.showInfo();

	return 0;
}

 运行结果:

------------------     构造两个Person对象     -----------------------
构造函数
构造函数
name:Maxine Caulfield
age:18
name:Chloe Price
age:19
------------------测试编译器自动生成的移动赋值-----------------------
右值引用移动赋值重载
name:Chloe Price
age:19
name:Maxine Caulfield
age:19
------------------测试编译器自动生成的移动构造-----------------------
右值引用移动复制构造
name:
age:19
name:Chloe Price
age:19

 

        在Person类中,我只实现了默认成员函数中的构造函数,当执行man1 = move(man2);这条语句时发现自行模拟实现的string类的移动赋值函数被调用了,说明编译器为我们的Person类自动生成了一个移动赋值重载函数,调用了我们string类的移动赋值。默认生成的移动赋值重载的功能就相当于我在Person类内注释掉的移动赋值重载:

	Person& operator=(Person&& p)
	{
		cout << "自行实现了移动赋值" << endl;
		_name.swap(p._name);
		_age = p._age;
		return *this;
	}

        当我们在执行Person man3(move(man1));这条语句时,编译器为我们的Person类自动生成了一个移动构造,其功能就相当于我在Person类中注释掉的移动构造一样:

	Person(Person&& p)
	{
		cout << "自行实现了移动构造" << endl;
		_name.swap(p._name);
		_age = p._age;
	}

        如果我们在Person类内把注释掉的任意一个函数给放出来,那么编译器就无法自动生成默认的移动构造移动赋值了,上面的两条语句 man1 = move(man2); 和 Person man3(move(man1));都会变为调用普通的赋值重载函数和拷贝构造函数。

        下面是如果把析构函数给放出来的运行结果:

------------------     构造两个Person对象     -----------------------
构造函数
构造函数
name:Maxine Caulfield
age:18
name:Chloe Price
age:19
------------------测试编译器自动生成的移动赋值-----------------------
赋值构造
name:Chloe Price
age:19
name:Chloe Price
age:19
------------------测试编译器自动生成的移动构造-----------------------
复制构造
name:Chloe Price
age:19
name:Chloe Price
age:19
自行实现了析构函数
自行实现了析构函数
自行实现了析构函数
 

 (二)、类成员变量初始化

        C++11允许在类的成员变量中使用缺省值进行初始化,在对象调用构造函数时就会视情况使用这些缺省值对成员进行初始化。

        来看下面这个例子:

        在调试时,依次执行的语句顺序是:226行、219行、213行、220行。最终结果是_a = 4、_b = 2、c = 6。因为构造函数的参数初始化表只对_a_c显式初始化,所以编译器就会使用_b的缺省值也就是2对_b进行初始化

(三)、default和delete关键字

1、default

        C++11可以让我们更好的控制类中默认生成的成员函数。比如我们要使用某个编译器自动生成的默认成员函数,但是因为某种原因编译器不能够自动生成,那么就可以使用default关键字让编译器强制生成我们想要的默认成员函数

        就比如说上面的Person类,如果显式实现了析构函数,那么编译器则不会自动生成默认的移动构造和移动赋值函数。但是我们可以用default关键字让编译器强制生成默认的移动构造默认的移动赋值函数

        下面来看default的用法:

//喜闻乐见的Person类
class Person
{
private:
	MyString::string _name;	//自定义类型
	int _age;	//内置类型
public:
	Person(const char* name, int age) :_name(name), _age(age) {}	//构造函数
	
	//使用default让编译器强制生成默认的移动构造和移动赋值函数
	Person(Person&& p) = default;	//让编译器强制生成默认的移动构造
	Person& operator=(Person&& p) = default;	//让编译器强制生成默认的移动赋值重载

	void showInfo()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

	~Person()
	{
		cout << "自行实现了析构函数" << endl;
	}
};

int main()
{
	cout << "------------------     构造两个Person对象     -----------------------" << endl;
	Person man1("Maxine Caulfield", 18);
	Person man2("Chloe Price", 19);

	man1.showInfo();
	man2.showInfo();

	cout << "------------------测试编译器自动生成的移动赋值-----------------------" << endl;
	//测试编译器自动生成的移动赋值
	man1 = move(man2);

	man1.showInfo();
	man2.showInfo();

	cout << "------------------测试编译器自动生成的移动构造-----------------------" << endl;
	Person man3(move(man1));
	man1.showInfo();
	man3.showInfo();

	return 0;
}

        运行结果: 

------------------     构造两个Person对象     -----------------------
构造函数
构造函数
name:Maxine Caulfield
age:18
name:Chloe Price
age:19
------------------测试编译器自动生成的移动赋值-----------------------
右值引用移动赋值重载
name:Chloe Price
age:19
name:Maxine Caulfield
age:19
------------------测试编译器自动生成的移动构造-----------------------
右值引用移动复制构造
name:
age:19
name:Chloe Price
age:19
自行实现了析构函数
自行实现了析构函数
自行实现了析构函数

 

        想让哪个默认成员函数自动生成,就要声明该默认成员函数并在后头加上“ = default ”

2、delete 

         delete的作用和default相反,用于删除某个默认的成员函数,让编译器无法生成指定的默认成员函数。常见用法就是将类中的默认拷贝构造函数delete掉,这样该类的任何对象都不能够被拷贝了,比如说C++中的流输出对象cout是不能够被拷贝的。

        使用举例:

//喜闻乐见的Person类
class Person
{
private:
	MyString::string _name;	//自定义类型
	int _age;	//内置类型
public:
	Person(const char* name, int age) :_name(name), _age(age) {}	//构造函数

	//使用delete将拷贝构造函数删除掉,
	// 让编译器无法生成默认的拷贝构造函数
	Person(const Person&) = delete;
	
	void showInfo()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
};

int main()
{
	Person man1("Maxine Caulfield", 18);
	Person man2(man1);	//尝试调用默认拷贝构造
	return 0;
}

        编译报错:拷贝函数被删除,无法将man1拷贝到man2上去,(移动赋值也不可以,除非移动复制写了default来默认生成)。

(四)、final和override关键字

        这两个关键字的用法和上面的defaultdelete一样,只是功能不同,finaloverride是用于类中的继承与多态的。

        override用于检测派生类中的虚函数是否完成了重写,如果没有完成重写会在编译过程中报错。使用方法就是在函数后面加上即可。

        final用于修饰某个虚函数,作用是使该虚函数所在的类之后的派生类中再也不能够被重写。使用方法和override相同。

        有关继承和多态的讲解可以看看我之前的博客:)[C++]多态与虚函数-CSDN博客

七、可变参数模板

1、概念

        C++11 引入的可变参数模板(variadic templates)是一个非常重要的特性,它极大地增强了模板的灵活性和表达能力。在 C++98/03 中,模板只能接受固定数量的模板参数,这限制了模板的通用性和灵活性。而可变参数模板允许模板接受任意数量的参数,从而可以编写更加通用和灵活的代码。

        可变参数模板在函数模板中的使用非常直观。通过 template<typename... Args> 语法,可以定义一个接受任意数量参数的函数模板。args就是一个参数包,里面包含了多个不同类型的多个参数

// 可变参数模板函数  
template<typename... Args>
void showList(Args... args) {
	cout << sizeof...(args) << endl;	//打印args里的参数个数
}

int main() {
	int a = 1;
	char b = 'b';
	double c = 1.1;

	showList(a);  
	showList(a, b);
	showList(a, b, c);
	return 0;
}

        运行结果:

1
2
3
 

2、参数包展开        

        如果要获取到参数包里的每个参数,那么要对参数包展开。

        在可变参数模板中,参数包的展开通常需要使用递归模板逗号表达式结合初始化列表折叠表达式(C++17 引入)

递归函数方式展开参数包

        方法如下

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

// 展开参数包函数
template<class T,class... Args>
void showList(T t,Args... args) {
	cout << t << " ";
	showList(args...);
}

int main() {
	int a = 1;
	char b = 'b';
	double c = 1.1;

	showList(a);  
	showList(a, b);
	showList(a, b, c);
	return 0;
}

        运行结果:

1
1 b
1 b 1.1

 

        递归的过程是在编译过程递归的,而不是在程序运行起来时动态的递归。下面来演示一下对showList(a, b, c);展开的递归过程

 逗号表达式结合初始化列表 

        使用这种方式展开参数包不需要递归终止函数,需要借助C++11列表初始化来初始化一个变长数组辅助展开参数包

// 这是我们要对每个参数调用的打印函数  
template<typename T>
void printArg(const T& arg) {
	cout << arg << " ";
}

// 可变参数模板函数,使用逗号表达式展开参数包  
template<typename... Args>
void printAll(Args&&... args) {
	// 创建一个只包含0的数组,也可以不为0,同时利用逗号表达式调用printArg
	// 注意:dummy数组在这里仅用于触发逗号表达式的求值,我们并不关心它的内容。  
	// 在实际使用中,这个数组可以被立即忘记或丢弃。  
	int dummy[] = { (printArg(args),0)...};
	cout << endl;

	//如果传入的参数是(3, 3.1415926, "=-="),可以理解为
	int dummyArr[] = { (printArg(3),0),(printArg(3.1415926),0) ,(printArg("=-="),0) };
}

int main() {
	printAll(1, 2.5, "Hello");
	return 0;
}

        运行结果: 

1 2.5 Hello
3 3.14159 =-=

        另外一种等价的写法:

// 这是我们要对每个参数调用的打印函数  
template<typename T>
int printArg(const T& arg) {
	cout << arg << " ";
	return 0;	//这里也可以return其他整形
}

// 可变参数模板函数
template<typename... Args>
void printAll(Args&&... args) {
	// 创建一个只包含0的数组,也可以不为0,同时利用printArg的返回值初始化变长数组
	int dummy[] = { printArg(args)... };
	cout << endl;

	//如果传入的参数是(3, 3.1415926, "=-="),可以理解为
	int dummyArr[] = { printArg(3),printArg(3.1415926) ,printArg("=-=") };
}

int main() {
	printAll(1, 2.5, "Hello");
	return 0;
}

3、emplace接口的理解

        在初步了解了可变参数模板后,可以来看看当初的emplace接口了,emplace接口融合了可变参数模板万能引用、完美转发

        模拟实现的string类有这样一个接口:用n个字符初始化字符串

string(size_t n, char ch);	//用n个ch字符初始化字符串
int main()
{
	MyList::list<MyString::string> myList;
	MyString::string str1("aaaaaaaaaaaaaaaaaa");
	cout << "---------------------------------" << endl;
	//使用empalce_back 进行尾插
	//-------------------------------下面这两种用法和push_back一样----------------------------------
	//尾插左值
	myList.emplace_back(str1);

	//尾插右值
	myList.emplace_back(MyString::string("bbbbbbbbbbbbbbbbb"));
	
	//-------------------------------下面的用法emplace_back独有----------------------------------
	//传递构造string所需的参数(n个字符)来进行尾插
	myList.emplace_back(15, 'c');

	cout << "遍历链表 " << endl;
	for (auto& e : myList)
	{
		cout << e << "->";
	}
	cout << "nullptr" << endl;

	return 0;
}

        运行结果: 

构造函数
复制构造
构造函数
---------------------------------
复制构造
构造函数
右值引用移动复制构造
遍历链表
aaaaaaaaaaaaaaaaaa->bbbbbbbbbbbbbbbbb->ccccccccccccccc->nullptr

 

        我们发现,最后一个调用empalce_back的参数是(10, 'c'),这就是和push_back的不同之处了,我们的emplace_back除了可以用某个对象构造新的对象,还可以传递构造对象所需的参数包来构造新的对象

        这在list中是怎么实现的呢?来看下面的演示图:

emplace接口相比push_back的好处

int main()
{
	MyList::list<MyString::string> myList;

	myList.push_back("111111");
	myList.emplace_back("111111");
	return 0;
}

        push_back尾插"111111"需要通过单参数的构造函数构造出一个string类的临时对象,然后通过该对象进行移动构造,构造出新的对象。过程为:一次构造 + 移动构造

        emplace_back可以将参数“111111”传递到构造list结点那一层,再把构造string类参数传给string进行构造,直接用参数“111111”构造出一个新对象。过程为:一次构造。

        所以我们在有了emplace后,应该更多的使用emplace接口。

八、lambda表达式(Lambda expressions)

(一)、lambda表达式的引入

        如果在vector数组里存储的元素是自定义类型,当我们要使用库函数sort(头文件:<algorithm>)对vector数组里的自定义类型的元素进行特殊需求的排序时,需要在外部提供该对象的比较方法,也就是仿函数

        那么如果要对学生类的姓名、年龄、身高、学分绩等分别进行排序,那么就会写出下面的代码:

//学生类
class Student
{
public:
	string _name;	//学生的名字
	int _age;	//年龄
	float _height;	//身高,单位:厘米(cm)
	float _score;	//学分绩

	Student(const string& name, int age, float height, float score) :
		_name(name), _age(age), _height(height), _score(score)
	{}

	void showInfo()
	{
		printf("name:%16s age:%2d height:%3.2f score:%2.1f\n",
			_name.c_str(), _age, _height, _score);
	}
};

//-------------------------------仿函数定义部分-----------------------------

//按照名字的字母表顺序排序
class sortByName
{
public:
	bool operator()(const Student& stu1, const Student& stu2)
	{
		return stu1._name < stu2._name;
	}
};

//按照年龄排序
class sortByAge
{
public:
	bool operator()(const Student& stu1, const Student& stu2)
	{
		return stu1._age < stu2._age;
	}
};

//按照身高排序
class sortByHeight
{
public:
	bool operator()(const Student& stu1, const Student& stu2)
	{
		return stu1._height < stu2._height;
	}
};

//按照学分绩排序
class sortByScore
{
public:
	bool operator()(const Student& stu1, const Student& stu2)
	{
		return stu1._score < stu2._score;
	}
};

int main()
{
	//4个学生
	vector<Student> students = {
		{"Max Caulfield", 18,165.0f,72.5f},
		{"Chloe Price", 19,175.3f,43.5f},
		{"Rachel Amber", 17,165.5f,90.2f},
		{"Kate Marsh", 20,167.0f,88.6f}
	};

	//使用库函数sort对4个学生排序,头文件:<algorithm>
	cout << "------------------按照名字字母表顺序排序----------------------" << endl;
	sort(students.begin(), students.end(), sortByName());
	for (auto& e : students)
	{
		e.showInfo();
	}

	cout << "------------------按照年龄大小排序----------------------------" << endl;
	sort(students.begin(), students.end(), sortByAge());
	for (auto& e : students)
	{
		e.showInfo();
	}

	cout << "------------------按照身高高低排序----------------------------" << endl;
	sort(students.begin(), students.end(), sortByHeight());
	for (auto& e : students)
	{
		e.showInfo();
	}

	cout << "------------------按照学分多少排序----------------------------" << endl;
	sort(students.begin(), students.end(), sortByScore());
	for (auto& e : students)
	{
		e.showInfo();
	}
	return 0;
}

        运行结果:

        C++11觉得,对于每一种不同的比较方法,都要对应定义一个来充当仿函数,这样很不方便和简介,于是就出现了lambda表达式,可以在原地生成一个匿名的函数对象

        下面使用lambda表达式提供匿名函数对象,对上面的代码进行改造:

//学生类
class Student
{
public:
	string _name;	//学生的名字
	int _age;	//年龄
	float _height;	//身高,单位:米(m)
	float _score;	//学分绩

	Student(const string& name, int age, float height, float score) :
		_name(name), _age(age), _height(height), _score(score)
	{}

	void showInfo()
	{
		printf("name:%16s age:%2d height:%3.2f score:%2.1f\n",
			_name.c_str(), _age, _height, _score);
	}
};

int main()
{
	//4个学生
	vector<Student> students = {
		{"Max Caulfield", 18,165.0f,72.5f},
		{"Chloe Price", 19,175.3f,43.5f},
		{"Rachel Amber", 17,165.5f,90.2f},
		{"Kate Marsh", 20,167.0f,88.6f}
	};

	//使用库函数sort对4个学生排序,头文件:<algorithm>
	cout << "------------------按照名字字母表顺序排序----------------------" << endl;
	sort(students.begin(), students.end(), 
		[](const Student& stu1, const Student& stu2)->bool
		{
			return stu1._name < stu2._name; 
		}
	);
	for (auto& e : students)
	{
		e.showInfo();
	}

	cout << "------------------按照年龄大小排序----------------------------" << endl;
	sort(students.begin(), students.end(),
		[](const Student& stu1, const Student& stu2)->bool
		{
			return stu1._age < stu2._age;
		}
	);
	for (auto& e : students)
	{
		e.showInfo();
	}

	cout << "------------------按照身高高低排序----------------------------" << endl;
	sort(students.begin(), students.end(),
		[](const Student& stu1, const Student& stu2)->bool
		{
			return stu1._height < stu2._height;
		}
	);
	for (auto& e : students)
	{
		e.showInfo();
	}

	cout << "------------------按照学分多少排序----------------------------" << endl;
	sort(students.begin(), students.end(),
		[](const Student& stu1, const Student& stu2)->bool
		{
			return stu1._score < stu2._score;
		}
	);
	for (auto& e : students)
	{
		e.showInfo();
	}
	return 0;
}

        运行结果:和上面的那一个一模一样。 

(二)、lambda表达式的使用细节

        lambda表达式的作用就是返回一个匿名函数对象。

使用原型:

[capture-list] (parameters) mutable -> return-type

{

         statement...        

}

capture-list:

1、捕获列表用于指定lambda表达式体内可以访问的外部变量。

2、捕获列表可以包含以下类型的捕获:

        值捕获:通过复制外部变量的值到lambda表达式内部。例如[x]或[=x](=表示对x进行值捕获)。

        引用捕获:通过引用方式捕获外部变量。例如[&x]或[&=x](&表示对x进行引用捕获)。

        混合捕获:同时使用值和捕获引用捕获。例如[=, &y](捕获所有外部变量为值,但y为引用)。

3、如果捕获列表为空,则写为[]。

parameters:

1、参数列表定义了lambda表达式的输入参数,就像普通函数的参数列表一样。

        例如(int a, int b)表示lambda表达式接受两个整型参数a和b。

2、如果lambda表达式不接受任何参数,参数列表可以为空,且可以连同()一起省略。

mutable:

1、mutable关键字是可选的。如果使用了mutable,lambda表达式体内的值捕获变量可以被修改。

2、默认情况下,值捕获的变量在lambda表达式体内是只读的(const修饰)。

3、使用mutable的话,前面部分的()不能够省略,即使没有任何参数。

-> return-type:

1、返回类型部分指定了lambda表达式的返回类型。

2、在很多情况下,返回类型可以省略,编译器会自动推导返回类型(基于函数体的返回语句)。

        例如-> int表示lambda表达式返回一个整型值。

        如果省略返回类型且函数体只有一个返回语句,编译器会根据该返回语句的类型推导返回类型。

statement...:

1、函数体部分包含了lambda表达式的实现代码。可以包含任意数量的语句,包括声明局部变量、控制流语句(如if、for等)和返回语句。

         lambda表达式的几种示例:

int main()
{
	cout << "-----------------------lambda1()---------------------------------" << endl;
	//一个最简单的lambda表达式,调用什么也不发生
	auto lambda1  = [] {};
	lambda1();

	cout << "-----------------------lambda2()---------------------------------" << endl;
	//省略了参数列表和返回值,返回值可以由编译器自动推导
	int a = 1, b = 2;
	auto lambda2 = [=] {	// [=] 等号值捕获,捕获a和b到lambda表达式内
		//a++;	//不允许,值捕捉到a的类型为const int
		//b++;	//不允许修改,值捕捉到b的类型为const int
		return a + b; 
		};	
	cout << lambda2() << endl;

	cout << "-----------------------lambda3()---------------------------------" << endl;
	//省略返回值类型
	auto lambda3 = [&] (int c){	//[&引用捕获:通过引用方式捕获外部变量a和b。
		a++;
		return a + b + c;	//编译器推导出返回值为int
		};
	cout << lambda3(2) << endl;
	cout << "a = " << a << endl;

	cout << "-----------------------lambda4()---------------------------------" << endl;
	//省略mutable
	auto lambda4 = [=, &b](int c)->int {
		//[=,&b]混合捕捉,
		//除了b(还可以写更多,用逗号隔开即可)是引用捕捉外其他变量采用值捕捉,
		b = a + c;
		return b;
		};
	cout << lambda4(3) << endl;
	cout << "b = " << b << endl;

	cout << "-----------------------lambda5()---------------------------------" << endl;
	//任何部分都不省略
	auto lambda5 = [=](int c)mutable->int
		{
			//mutable用于解除值传递的const限制
			a += 5;
			b += 5;
			cout << "a = " << a << " b = " << b << " c = " << c << endl;
			return a + b + c;
		};
	cout << lambda5(4) << endl;
	cout << "a = " << a << " b = " << b << endl;
	return 0;
}

        运行结果: 

-----------------------lambda1()---------------------------------
-----------------------lambda2()---------------------------------
3
-----------------------lambda3()---------------------------------
6
a = 2
-----------------------lambda4()---------------------------------
5
b = 5
-----------------------lambda5()---------------------------------
a = 7 b = 10 c = 4
21
a = 2 b = 5

 

捕获列表的说明:

1、具体变量捕获(一个个写出要捕获的变量,通过逗号隔开):

        [var]:表示以值传递的方式捕获变量var。

        [&var]:表示以引用传递的方式捕获变量var。

2、批量捕获:

        [=]:表示以值传递的方式捕获所有父作用域(即包含lambda表达式的语句块)中的变量,包括类的成员函数中的this指针(如果lambda表达式定义在类的成员函数内部)。

        [&]:表示以引用传递的方式捕获所有父作用域中的变量,同样包括this指针。

3、混合捕获:

        捕获列表可以包含多个捕获项,这些项之间以逗号分隔。例如,[=, &a, &b]表示以值传递的方式捕获所有变量,但以引用传递的方式特别捕获a和b。

        需要注意的是,混合捕获时要避免重复捕获同一变量,否则会导致编译错误。例如,[=, a]就是错误的,因为=已经捕获了所有变量(包括a)。

4、捕获限制:

        捕获列表不允许重复捕获同一变量。

        在块作用域以外的lambda表达式中,捕获列表必须为空,因为此时没有外部变量可供捕获。

        在块作用域中的lambda表达式只能捕获其父作用域中的局部变量。尝试捕获任何非此作用域或非局部变量的行为都会导致编译错误。

        全局变量也可以捕获到。

5、其他注意事项:

        Lambda表达式之间不能相互赋值,即使它们看起来类型相同。这是因为lambda表达式的类型是由编译器根据捕获列表、参数列表和函数体唯一确定的,不同的lambda表达式即使看起来相似,它们的类型也可能是不同的。 

九、包装器

(一)、包装器的引入

        到现在,可以使用函数指针仿函数的对象lambda表达式的方法来调用函数,也就是说可以将函数指针、仿函数的对象、lambda表达式对象当参数传递到别的函数里,让别的函数能够调用到我们定义的方法,虽然定义方法的有三种之多,但是这样可能会导致模板的效率下降(实例化多份模板函数)。        

        我们来看下面这个场景:


template<class F,class T>
T callFunc(F func,T x)
{
	static int n = 0;		//声明一个static变量,每一个用模板实例化的函数都有自己的n
	cout << "&n = " << &n << endl;	//打印该变量的内存地址

	auto ret = func(x);
	return ret;
}

//普通的函数
double funcNormal(double a)
{
	return a / 2;
}

//函数对象
struct funcClass
{
	double operator()(double a)
	{
		return a / 3;
	}
};

//lambda表达式对象
auto funcLambda = [](double a)->double {return a / 4; };

int main()
{
	//分别传入指针、函数对象、lambda表达式对象实例化callFunc函数

	cout << "---------------------传入函数指针-----------------------------" << endl;
	cout << callFunc(funcNormal, 10.0) << endl;
	cout << "---------------------传入函数对象-----------------------------" << endl;
	cout << callFunc(funcClass(), 10.0) << endl;
	cout << "---------------------传入lambda对象---------------------------" << endl;
	cout << callFunc(funcLambda, 10.0) << endl;
	return 0;
}

        运行结果:

 ---------------------传入函数指针-----------------------------
&n = 00007FF6CDE1845C
5
---------------------传入函数对象-----------------------------
&n = 00007FF6CDE18460
3.33333
---------------------传入lambda对象---------------------------
&n = 00007FF6CDE18464
2.5

        我们通过打印static的n的地址发现,callFunc模板函数因为传入的函数方法不同,函数模板被实例化了三份,但被实例化三份函数内调用的函数形态几乎完全相同,其实只需要实例化一份就足够了,而我们的包装器就能解决这个问题。

(二)、function(包装器)的使用 

        C++11中的function叫做包装器,头文件为<fuctional>,能够将任何类型的可调用元素(如函数和函数对象)包装成一个可复制对象的类,而这个类的类型仅取决于其调用签名(而不是被包装的可调用元素本身的类型)。

        一个fuction类实例化的对象可以包装以下任何一种类型的可调用对象:函数、函数指针、成员函数指针,或者任何类型的函数对象(即,其类定义了operator()的对象,包括lambda表达式)

        我们来看一下function模板类的声明:

Ret: 被调用函数的返回类型

Args…:被调用函数的形参

        我们来试着用function包装一下我们上面的三种函数方法

//普通的函数
double funcNormal(double a)
{
	return a / 2;
}

//函数对象
struct funcClass
{
	double operator()(double a)
	{
		return a / 3;
	}
};

//lambda表达式对象
auto funcLambda = [](double a)->double {return a / 4; };

int main()
{
	//实例化三个包装器对象,对函数方法进行包装
	//可以看出,包装好方法的f1、f2和f3的类型相同
	function<double(double)> f1 = funcNormal;
	function<double(double)> f2 = funcClass();
	function<double(double)> f3 = funcLambda;

	//实例化好后,使用包装器调用包装好的方法
	cout << f1(10) << endl;
	cout << f2(10) << endl;
	cout << f3(10) << endl;
	return 0;
}

        运行结果: 

5
3.33333
2.5

 

        我们再来看看类的成员函数的该如何包装: 

//随手写的A类
class A
{
public:
	//静态成员函数
	static int staticAdd(int x,int y)
	{
		return x + y;
	}

	//非静态成员函数
	double noStaticAdd(double x, double y)
	{
		return x + y;
	}
};

int main()
{
	//--------------------------包装静态成员函数----------------------------------
	function<int(int, int)> f1 = A::staticAdd;
	cout << f1(1,2) << endl;

	//--------------------------包装非静态成员函数--------------------------------
	
	//A* 可以理解为成员函数的参数中隐藏了的this指针
	//注意:成员函数需要用&取一下地址才能得到地址
	function<double(A*, double, double)> f2 = &A::noStaticAdd;	
	A a;	//实例化一个对象
	cout << f2(&a, 1.1, 2.2) << endl;

	//另一种包装方法
	//将第一种方法的A* 换成 A
	function<double(A, double, double)> f3 = &A::noStaticAdd;
	cout << f3(a, 2.2, 3.3) << endl;
	return 0;
}

        运行结果: 

 3
3.3
5.5

 (三)、包装器的使用场景

场景一:优化模板实例化次数

        现在我们使用包装器对上面的callFunc模板函数进行一下优化。

template<class F, class T>
T callFunc(F func, T x)
{
	static int n = 0;		//声明一个static变量,每一个用模板实例化的函数都有自己的n
	cout << "&n = " << &n << endl;	//打印该变量的内存地址

	auto ret = func(x);
	return ret;
}

//普通的函数
double funcNormal(double a)
{
	return a / 2;
}

//函数对象
struct funcClass
{
	double operator()(double a)
	{
		return a / 3;
	}
};

//lambda表达式对象
auto funcLambda = [](double a)->double {return a / 4; };

//本来我们是给callFunc传入的三个不同的函数方法
int oldMain()
{
	//分别传入指针、函数对象、lambda表达式对象来实例化callFunc函数

	cout << "---------------------传入函数指针-----------------------------" << endl;
	cout << callFunc(funcNormal, 10.0) << endl;
	cout << "---------------------传入函数对象-----------------------------" << endl;
	cout << callFunc(funcClass(), 10.0) << endl;
	cout << "---------------------传入lambda对象---------------------------" << endl;
	cout << callFunc(funcLambda, 10.0) << endl;
	return 0;
}

//现在我们给callFunc传入包装好的一样的包装器对象
int main()
{
	//实例化三个包装器对象,对函数方法进行包装
	//可以看出,包装好方法的f1、f2和f3的类型相同
	function<double(double)> f1 = funcNormal;
	function<double(double)> f2 = funcClass();
	function<double(double)> f3 = funcLambda;

	//传入callFunc里调用
	cout << callFunc(f1, 10.0) << endl;
	cout << callFunc(f2, 10.0) << endl;
	cout << callFunc(f3, 10.0) << endl;
	return 0;
}

        运行结果: 

&n = 00007FF605C09820
5
&n = 00007FF605C09820
3.33333
&n = 00007FF605C09820
2.5

 

        现在,我们每次调用callFunc打印static的变量n,打印出一样的地址,说明callFunc函数只实例化了一份,优化了模板效率。 

场景二:搭配map使用

        在一些MOBA游戏中,我们可能会有这样的需求:玩家按下某个技能后,程序需要通过技能的名字找到对应的函数执行相应的逻辑,技能的名字和函数就形成了映射关系。

        我们可以将map搭配包装器完成我们的需求:

//这个类存储当前玩家操控的角色的各种属性
struct CharacterInfo
{
	int hp = 100;	//生命值
	int mp = 100;	//法力值
	//.....
};

//LOL易大师的技能:
//Q:阿尔法突袭
bool AlphaStrike(CharacterInfo info)
{
	if (info.mp > 0)	//目前逻辑就这么简单,有魔力就可以放出技能
	{
		cout << "阿尔法突袭!" << endl;
		return true;
	}
	else
		return false;
}

//W:冥想
bool Meditate(CharacterInfo info)
{
	if (info.mp > 0)	//目前逻辑就这么简单,有魔力就可以放出技能
	{
		cout << "冥想~~~~~" << endl;
		return true;
	}
	else
		return false;
}

//E:无极之道
bool WujuStyle(CharacterInfo info)
{
	if (info.hp > 0)	//目前逻辑就这么简单,有血量就可以放出技能
	{
		cout << "无极之道!" << endl;
		return true;
	}
	else
		return false;
}

//R:高原血统
bool Highlander(CharacterInfo info)
{
	if (info.mp > 0)	//目前逻辑就这么简单,有魔力就可以放出技能
	{
		cout << "高原血统!" << endl;
		return true;
	}
	else
		return false;
}

//还可以写一些别的英雄的技能......


int main()
{
	map<std::string, function<bool(CharacterInfo)>> skillFuncMap;
	skillFuncMap.insert({ "阿尔法突袭",AlphaStrike });
	skillFuncMap.insert({ "冥想",Meditate });
	skillFuncMap.insert({ "无极之道",WujuStyle });
	skillFuncMap.insert({ "高原血统",Highlander });

	CharacterInfo character;

    //通过技能名字执行对应的函数
	skillFuncMap["阿尔法突袭"](character);
	skillFuncMap["冥想"](character);
	skillFuncMap["无极之道"](character);
	skillFuncMap["高原血统"](character);
	return 0;
}

        运行结果: 

阿尔法突袭!
冥想~~~~~
无极之道!
高原血统!

 

        我们游戏里的英雄技能是有很多很多的,把他们都放进map里,技能名跟函数建立映射关系,这样管理(后续增加新的英雄技能)和使用起来方便且效率也很高。

(四)、bind(绑定)

        C++11中的bind它位于<functional>头文件中。bind函数的主要作用是将一个可调用对象(如函数、函数对象、成员函数指针或另一个bind表达式)的参数在bind中用值指定(额外参数)占位符(_1,_2....)跟新对象传递的参数进行绑定,然后生成一个新的可调用对象。 

        这个新的可调用对象在调用时会将预先绑定的参数调用时提供的任何额外参数一起传递给原始的可调用对象。如果绑定的参数是占位符(std::placeholders::_1, std::placeholders::_2, ...),则这些占位符在调用时会被替换为传递给绑定器的相应参数。

        作用大概就是:当你从某处获得了一个接口,接口调用需要传入非常多的参数,每次调用时只有几个位置的参数是需要变动的,而你又不能修改接口的参数个数时,就可以使用绑定,通过这个原始的接口绑定出一个新的对象,每次调用这个对象只需要传定义好的比原始接口要少的参数,并且传入参数的位置还可以调换。

        概念看不懂没关系,可以直接看例子:

//用于依次打印参数的值
void display(int a, int b, int c)
{
	cout << a << " " << b << " " << c << " " << endl;
}

//测试用的类
struct MyClass
{
	int _data = 5;
	void show(int a, int b)
	{
		cout << "a = " << a << " b = " << b << endl;
	}
};


int main()
{
	//展开占位符所在的命名空间
	using namespace std::placeholders;    // 占位符(_1,_2,_3...)所在的命名空间域

	//如果需要调用display,我们需要传入三个参数
	display(1, 2, 3);

	//下面使用绑定生成一个新对象来调用display
	cout << "----------------场景一-----------------------------" << endl;

	//场景一:在绑定时指定完所有的参数,在调用时不需要传参
	function<void()> f1 = bind(display, 4, 5, 6);	
	cout << "场景一:f1()" << endl;
	f1();	//啥也不传,也能调动display函数

	cout << "----------------场景二-----------------------------" << endl;
	//场景二:在绑定时指定一些参数,其余参数从新对象中获得,在调用时需要传入少量参数
	function<void(int)> f2 = bind(display, _1, 8, 9);
	cout << "场景二:f2(1000)" << endl;
	f2(1000);

	function<void(int, int)> f3 = bind(display, _1, _2, 12);
	cout << "场景二:f3(1000, 2000)" << endl;
	f3(1000, 2000);
	
	cout << "----------------场景三-----------------------------" << endl;
	//场景三:在场景二的基础上,将占位符的位置调换到不同位置
	function<void(int)> f4 = bind(display, 7, _1, 9);
	cout << "场景三:f4(1000)" << endl;
	f4(1000);

	function<void(int)> f5 = bind(display, 7, 8, _1);
	cout << "场景三:f5(1000)" << endl;
	f5(1000);

	function<void(int,int)> f6 = bind(display, _2, 8, _1);
	cout << "场景三:f6(1000,2000)" << endl;
	f6(1000,2000);


	cout << "----------------场景四-----------------------------" << endl;
	//场景四:绑定时不指定参数,全部由新对象提供,但是位置变化
	function<void(int, int, int)> f7 = bind(display, _2, _3, _1);
	cout << "场景四:f7(1000,2000,3000)" << endl;
	f7(1000, 2000, 3000);


	cout << endl << endl;
	MyClass c;
	// 绑定成员函数
	function<void(int, int)> f8 = bind(&MyClass::show, c, _2, _1);	//这里指定了调用该成员函数的是对象c,每次调用show时this指针都会指向c
	f8(100, 200);
	cout << "c::_data = " << c._data << endl;

	//一种奇怪的用法,一般不会这么用的
	c._data = 55;	//先修改c对象内data的值
	function<int()> f9 = bind(&MyClass::_data, c);	//功能就是,从对象c中数据取出_data并返回
	cout << f9() << endl;

	return 0;
}

1 2 3
----------------场景一-----------------------------
场景一:f1()
4 5 6
----------------场景二-----------------------------
场景二:f2(1000)
1000 8 9
场景二:f3(1000, 2000)
1000 2000 12
----------------场景三-----------------------------
场景三:f4(1000)
7 1000 9
场景三:f5(1000)
7 8 1000
场景三:f6(1000,2000)
2000 8 1000
----------------场景四-----------------------------
场景四:f7(1000,2000,3000)
2000 3000 1000


a = 200 b = 100
c::_data = 5
55
 

       以上就是我对于C++11的一些新特性的理解和讲解。

模拟实现string类的代码

string.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1	//允许VS使用旧版<cstring>里的不安全的str系列函数
#include <iostream>
#include <cstring>
#include <assert.h>

using namespace std;


namespace MyString
{
	class string
	{
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
		
	public:
		//-------------------------------------------   构造  ------------------------------------------
		//构造函数
		string(const char* str = "");//如果啥都没,那就只有个'\0'
		string(size_t n, char ch);	//用n个ch字符初始化字符串

		//复制构造和赋值构造
		string(const string& s);
		string(string&& s);
		string& operator=(const string& s);
		string& operator=(string&& s);

		//---------------------------------------------迭代器--------------------------------------------
		const static size_t npos;

		//返回数组的指针
		const char* c_str() const;

		//迭代器
		typedef char* iterator;
		typedef const char* const_iterator;

		//迭代器
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;

		//---------------------------------------------容量相关--------------------------------------------

		//返回字符串的大小
		size_t size() const;

		//扩容
		void reserve(size_t n);

		//以数组方式访问[]
		char& operator[](size_t pos);
		const char& operator[](size_t pos) const;

		//---------------------------------------------修改-------------------------------------------------
		//尾插
		void push_back(char ch);
		void append(const char* str);
		void append(const string& str);

		//某个位置插入
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void insert(size_t pos, const string& str);

		//重载运算符+=
		string& operator+=(char ch);
		string& operator+=(const char* str);
		string& operator+=(const string& str);

		//某个位置删除
		void erase(size_t pos = 0, size_t len = npos);

		//字符串交换
		void swap(string& s);
		//---------------------------------------------字符串的操作-------------------------------------------------
		//查找
		size_t find(char ch, size_t pos = 0)const;
		size_t find(const char* str, size_t pos = 0)const;
		size_t find(const string& str, size_t pos = 0)const;

		//截取字符串
		string substr(size_t pos = 0, size_t len = npos)const;

		//字符串比较
		bool operator<(const string& s) const;
		bool operator>(const string& s) const;
		bool operator<=(const string& s) const;
		bool operator>=(const string& s) const;
		bool operator==(const string& s) const;
		bool operator!=(const string& s) const;
		void clear();	//清除字符串

		//释放字符串
		~string();
	};

	//流提取和流插入重载
	std::ostream& operator<< (std::ostream&, const string&);
	std::istream& operator>> (std::istream&, string&);
}


string.cpp

#include "String.h"

namespace MyString
{
	const size_t string::npos = -1;

	string::string(const char* str)//如果啥都没,那就只有个'\0'
	{
		cout << "构造函数" << endl;
		assert(str != NULL);
		size_t size = strlen(str);
		_size = size;
		_capacity = size;
		_str = new char[_capacity + 1];//预留一个位置存储\0 最少要申请1个空间来存储空字符串
		strcpy(_str, str);
	}

	string::string(size_t n, char ch)	//用n个ch字符初始化字符串
	{
		_str = new char[n+1];	//预留一个位置存'\0'
		_capacity = _size = n;
		for (int i = 0; i < n; i++)
		{
			_str[i] = ch;
		}
		_str[n] = '\0';
	}


	//返回数组的指针
	const char* string::c_str() const
	{
		return _str;
	}

	//迭代器
	string::iterator string::begin()
	{
		return _str;
	}
	string::iterator string::end()
	{
		return _str + _size;
	}

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

	//以数组方式访问[]
	char& string::operator[](size_t pos)
	{
		assert(pos < _size);
		return _str[pos];
	}

	const char& string::operator[](size_t pos) const
	{
		assert(pos < _size);
		return _str[pos];
	}

	//扩容
	void string::reserve(size_t n)
	{
		//不能缩容
		assert(n > _size);

		char* tem = new char[n + 1];
		strcpy(tem, _str);
		delete[] _str;
		_str = tem;
		_capacity = n;
	}

	//复制构造和赋值构造
	string::string(const string& s)
	{
		cout << "复制构造" << endl;
		_size = s._size;
		_capacity = s._capacity;

		_str = new char[_capacity + 1];
		strcpy(_str, s.c_str());
	}
	string& string::operator=(const string& s)
	{
		cout << "赋值构造" << endl;
		//防止自己给自己赋值
		if (this != &s)
		{
			delete[] _str;
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_capacity = s._capacity;
			_size = s._size;
		}
		
		return *this;
	}


	string::string(string&& s)
	{
		cout << "右值引用移动复制构造" << endl;
		swap(s);
	}

	string& string::operator=(string&& s)
	{
		cout << "右值引用移动赋值重载" << endl;
		swap(s);
		return *this;
	}

	//尾插
	void string::push_back(char ch)
	{
		//判断是否需要扩容
		if (_size == _capacity)
		{
			size_t newcapacity = _capacity == 0 ? 4 : _capacity * 1.5;
			reserve(newcapacity);
		}

		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';

	}
	void string::append(const char* str)
	{
		size_t length = strlen(str);
		if (_size + length > _capacity)
		{
			reserve(_size + length);
		}

		//追加字符串
		strcpy(_str + _size, str);
		_size += length;
	}


	//重载运算符+=
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	string& string::operator+=(const string& str)
	{
		append(str._str);
		return *this;
	}

	//某个位置插入
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		//判断是否需要扩容
		if (_size == _capacity)
		{
			size_t newcapacity = _capacity == 0 ? 4 : _capacity * 1.5;
			reserve(newcapacity);
		}

		//将pos位置后面的字符往后挪一位,包括'\0'
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			end--;
		}
		_str[pos] = ch;
		_size++;
	}
	void string::insert(size_t pos, const char* str)//追加字符串
	{
		size_t length = strlen(str);
		if (_size + length > _capacity)
		{
			reserve(_size + length);
		}

		//向后挪length位
		size_t end = _size + length;
		while (end > pos + length - 1)
		{
			_str[end] = _str[end - length];
			end--;
		}
		memcpy(_str + pos, str, length);
		_size += length;
	}
	void string::insert(size_t pos, const string& str)
	{
		insert(pos, str._str);
	}

	//某个位置删除
	void string::erase(size_t pos, size_t len)
	{
		//删除pos后的所有数据
		if (len == npos || pos + len >= _size)
		{
			_str[pos] = '\0';
			_size = pos;
			return;
		}
		memcpy(_str + pos, _str + pos + len, _size - pos - len);
		_str[_size - len] = '\0';
		_size -= len;
	}

	//查找
	size_t string::find(char ch, size_t pos) const
	{
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		
		return npos;
	}
	size_t string::find(const char* str, size_t pos) const
	{
		char* pfind = strstr(_str + pos, str);
		if (pfind != NULL)
			return pfind - _str;
		else
			return npos;
	}
	size_t string::find(const string& str, size_t pos) const
	{
		return find(str._str,pos);
	}

	//字符串交换
	void string::swap(string& s)
	{
		std::swap(_capacity, s._capacity);
		std::swap(_size, s._size);
		std::swap(_str, s._str);
	}

	//截取字符串
	string string::substr(size_t pos, size_t len)const
	{
		if (len == npos || pos + len >= _size)
		{
			return string(_str + pos);
		}

		string str;
		str.reserve(len);
		for (size_t i = pos; i < pos+len; i++)
		{
			str += _str[i];
		}

		return str;
	}

	//字符串比较
	bool string::operator<(const string& s) const
	{
		return strcmp(_str, s._str) < 0;
	}
	bool string::operator>(const string& s) const
	{
		return !((*this) < s) && !((*this) == s);
	}
	bool string::operator<=(const string& s) const
	{
		return !((*this) > s);
	}
	bool string::operator>=(const string& s) const
	{
		return !((*this) < s);
	}
	bool string::operator==(const string& s) const
	{
		return strcmp(_str, s._str) == 0;
	}
	bool string::operator!=(const string& s) const
	{
		return !(*this == s);
	}

	size_t string::size() const
	{
		return _size;
	}

	std::ostream& operator<<(std::ostream& output,const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			output << s[i];
		}
		return output;
	}
	//清空字符串
	void string::clear()
	{
		_size = 0;
		_str[0] = '\0';
	}

	std::istream& operator>>(std::istream& input, string& s)
	{
		s.clear();
		char ch;
		ch = input.get();
		while (ch != '\n' && ch != ' ')
		{
			s += ch;
			ch = input.get();
		}
		return input;
	}

	string::~string()
	{
		//cout << "delete:" << _str << endl;
		_capacity = _size = 0;
		delete[] _str;
	}
}

模拟实现list类的代码

list.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>


namespace MyList
{
	//链表节点
	template<class T>
	struct ListNode
	{
	public:
		T value;
		ListNode<T>* _pPrev;
		ListNode<T>* _pNext;
		//默认构造生成一个节点
		ListNode(const T& x = T())
			:value(x),
			_pPrev(nullptr),
			_pNext(nullptr)
		{
		}

		//右值移动构造
		ListNode(T&& x)
			:value(move(x)),
			_pPrev(nullptr),
			_pNext(nullptr)
		{
		}

		//参数包版本的构造
		template<class... Args>
		ListNode(Args... args)
			:value(std::forward<Args>(args)...),
			_pPrev(nullptr),
			_pNext(nullptr)
		{
		}
	};



	//迭代器
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
	public:
		typedef ListNode<T> Node;
		typedef ListNode<T>* PNode;
		typedef ListIterator<T, Ref, Ptr> Self;
		PNode _pNode;

		ListIterator(PNode pNode = nullptr) :_pNode(pNode) { ; }

		//重载++
		Self& operator++()
		{
			auto& _pNode = this->_pNode;
			_pNode = _pNode->_pNext;
			return *this;
		}
		Self operator++(int)
		{
			Self tem(_pNode);
			_pNode = _pNode->_pNext;
			return tem;
		}

		//重载--
		Self& operator--()
		{
			_pNode = _pNode->_pPrev;
			return *this;
		}
		Self operator--(int)
		{
			Self tem(_pNode);
			_pNode = _pNode->_pPrev;
			return tem;
		}

		//重载解引用
		Ref operator*() const
		{
			return _pNode->value;
		}
		//重载->
		Ptr operator->() const
		{
			return &(_pNode->value);
		}

		bool operator==(Self it) const
		{
			return _pNode == it._pNode;
		}
		bool operator!=(Self it) const
		{
			return _pNode != it._pNode;
		}

		
	};

	//适配反向迭代器
	template<class T, class Ref, class Ptr>
	struct ListReverseIterator
	{
	public:
		typedef ListReverseIterator<T, Ref, Ptr> Self;
		typedef ListNode<T> Node;
		typedef ListNode<T>* PNode;
		PNode _pNode;

		ListReverseIterator(PNode pNode = nullptr) :_pNode(pNode){; }

		//重载++
		Self& operator++()
		{
			_pNode = _pNode->_pPrev;
			return *this;
		}
		Self operator++(int)
		{
			Self tem(_pNode);
			_pNode = _pNode->_pPrev;
			return tem;
		}

		//重载--
		Self& operator--()
		{
			_pNode = _pNode->_pNext;
			return *this;
		}
		Self operator--(int)
		{
			Self tem(_pNode);
			_pNode = _pNode->_pNext;
			return tem;
		}

		//重载解引用
		Ref operator*() 
		{
			return _pNode->value;
		}
		//重载->
		Ptr operator->() 
		{
			return &(_pNode->value);
		}

		bool operator==(const Self& it) 
		{
			return _pNode == it._pNode;
		}
		bool operator!=(const Self& it) 
		{
			return _pNode != it._pNode;
		}
		
	};

	//带头双向循环list容器
	template<class T>
	class list
	{
	public:
		typedef ListNode<T> Node;
		typedef ListNode<T>* PNode;

		//迭代器
		typedef ListIterator<T,T&,T*> iterator;
		typedef ListIterator<T,const T&,const T*> const_iterator;
		//反向迭代器
		typedef ListReverseIterator<T, T&, T*> reverse_iterator;
		typedef ListReverseIterator<T, const T&,const T*> const_reverse_iterator;

		//初始化
		list()
		{
			_phead = new Node;
			_phead->_pNext = _phead;
			_phead->_pPrev = _phead;
		}

		list(int n, const T& value = T()):list()
		{
			for (int i = 0; i < n; i++)
			{
				push_back(value);
			}
		}
		template <class Iterator>
		list(Iterator first, Iterator last):list()
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		//参数初始化列表初始化
		list(std::initializer_list<T> ini_list):list()
		{
			for (auto& e : ini_list)
			{
				push_back(e);
			}
		}

		list(const list<T>& l):list()
		{
			//拷贝构造
			for (auto& e : l)
			{
				push_back(e);
			}
		}
		list<T>& operator=(const list<T>& l)
		{
			list<T> tem(l);
			swap(tem);
			return *this;
		}

		//一个一个节点销毁
		~list()
		{
			PNode pcur = _phead->_pNext;
			PNode ptem;//记录销毁节点的下一个节点
			//cout << "销毁:";
			while (pcur != _phead)
			{
				ptem = pcur->_pNext;
				//cout << pcur->value << "->";
				delete pcur;
				pcur = ptem;
			}
			//cout << "nullptr" << endl;
			_phead = nullptr;
		}

		//交换两个链表内容的操作
		void swap(list<T>& v1)
		{
			std::swap(_phead, v1._phead);
		}

		//迭代器
		iterator begin()
		{
			return iterator(_phead->_pNext);
		}
		iterator end()
		{
			return iterator(_phead);
		}
		const_iterator begin() const
		{
			return const_iterator(_phead->_pNext);
		}
		const_iterator end() const
		{
			return const_iterator(_phead);
		}

		//反向迭代器
		reverse_iterator rbegin()
		{
			return reverse_iterator(_phead->_pPrev);
		}
		reverse_iterator rend()
		{
			return reverse_iterator(_phead);
		}
		const_reverse_iterator rbegin() const
		{
			return const_reverse_iterator(_phead->_pPrev);
		}
		const_reverse_iterator rend() const
		{
			return const_reverse_iterator(_phead);
		}


		// List Capacity
		size_t size()const
		{
			PNode pcur = _phead->_pNext;
			size_t n = 0;
			while (pcur != _phead)
			{
				n++;
				pcur = pcur->_pNext;
			}
			return n;
		}
		bool empty()const
		{
			return _phead == _phead->_pNext;
		}

		// List Access
		T& front()
		{
			if (!empty())
				return _phead->_pNext->value;
			else
				return _phead->value;
		}
		const T& front()const
		{
			if (!empty())
				return _phead->_pNext->value;
			else
				return _phead->value;
		}
		T& back()
		{
			if (!empty())
				return _phead->_pPrev->value;
			else
				return _phead->value;
		}
		const T& back()const
		{
			if (!empty())
				return _phead->_pPrev->value;
		}


		//----------------------------------      修改     -------------------------------------
		// 描述:进行增删改等修改操作
		//--------------------------------------------------------------------------------------
		
		//尾插
		//万能引用 + 完美转发
		template<class Ref>
		void push_back(Ref&& x)
		{
			insert(end(),std::forward<Ref>(x));	//代码复用
		}

		//使用参数包尾插
		template<class... Args>
		void emplace_back(Args&&... args)
		{
			insert(end(), std::forward<Args>(args)...);
		}

		//头插
		//万能引用 + 完美转发
		template<class Ref>
		void push_fornt(Ref&& x)
		{
			insert(begin(), std::forward<Ref>(x));	//代码复用
		}

		//指定位置插入
		//不存在迭代器失效问题
		//万能引用 + 完美转发 -> 决定是拷贝构造还是移动构造
		template<class Ref>
		iterator insert(iterator pos, Ref&& x)
		{

			PNode newNode = new Node(std::forward<Ref>(x));
			newNode->_pNext = pos._pNode;
			newNode->_pPrev = pos._pNode->_pPrev;
			pos._pNode->_pPrev->_pNext = newNode;
			pos._pNode->_pPrev = newNode;
			return iterator(newNode);
		}

		//参数包可以代表一个值或多个值,写了参数包版本上面的那个版本的insert就可以删去了
		template<class... Args>
		iterator insert(iterator pos,Args&&... args)
		{

			PNode newNode = new Node(std::forward<Args>(args)...);
			newNode->_pNext = pos._pNode;
			newNode->_pPrev = pos._pNode->_pPrev;
			pos._pNode->_pPrev->_pNext = newNode;
			pos._pNode->_pPrev = newNode;
			return iterator(newNode);
		}



		//头删
		void pop_fornt()
		{
			if (empty())
				return;
				

			PNode pDel = _phead->_pNext;
			pDel->_pPrev->_pNext = pDel->_pNext;
			pDel->_pNext->_pPrev = pDel->_pPrev;
			delete pDel;
		}

		//尾删
		void pop_back()
		{
			if (empty())
				return;

			PNode tail = _phead->_pPrev;
			tail->_pPrev->_pNext = tail->_pNext;
			tail->_pNext->_pPrev = tail->_pPrev;
			delete tail;
		}

		//指定位置删除
		//返回删除位置的下一个节点的迭代器,因为会存在迭代器失效问题
		iterator erase(iterator pos)
		{
			iterator itNext(pos._pNode->_pNext);
			//连接前后节点
			pos._pNode->_pPrev->_pNext = pos._pNode->_pNext;
			pos._pNode->_pNext->_pPrev = pos._pNode->_pPrev;
			delete pos._pNode;
			return itNext;
		}
	private:
		//头节点指针
		PNode _phead;
		//size_t _length;
	};
}

        

        

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

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

相关文章

有关自连接表的统一封装

表结构 RecursionBean Getter Setter ToString JsonInclude(JsonInclude.Include.NON_EMPTY) public class RecursionBean<T> extends BaseVO {/*** 编号*/private T id;/*** 父权限ID&#xff0c;根节点的父权限为空* 注释掉JsonIgnore&#xff0c;是为了前端判断是否…

Linux驱动开发常用调试方法汇总

引言&#xff1a;在 Linux 驱动开发中&#xff0c;调试是一个至关重要的环节。开发者需要了解多种调试方法&#xff0c;以便能够快速定位和解决问题。 1.利用printk 描述&#xff1a; printk 是 Linux 内核中的一个调试输出函数&#xff0c;类似于用户空间中的 printf。它用于…

CE找CSGO人物坐标和视角基址-幽络源原创

前言 幽络源站长本次免费分享的是CE找CSGO人物坐标和视角基址 本教程分为两篇&#xff0c;当前为上篇->找基址 所具备的知识 CE的使用 教程目的 通过CE找到一些基地址&#xff0c;然后结合Python实现CSGO的透视绘制&#xff0c;这里我们是纯手写透视。 第一步&#x…

如何使用CMD命令启动应用程序(二)

说明&#xff1a;去年1024发布了一篇博客&#xff0c;介绍如何使用CMD命令启动应用程序&#xff0c;但实际情况&#xff0c;有些程序可能无法用配置环境变量的方式来启动&#xff0c;本文针对两种情况下的程序&#xff0c;如何使用CMD命令来启动&#xff0c;算是对上一篇博客的…

Java开发必知必会的一些工具

本文主要介绍 Java 程序员应该学习的一些基本和高级工具。 如果你想成为一名更好的程序员&#xff0c;最重要的技巧之一就是学习你的编程工具。 Java 世界中存在着如此多的工具&#xff0c;从 Eclipse、NetBeans 和 IntelliJ IDEA 等著名的 IDE 到 JConsole、VisualVM、Eclipse…

class 004 选择 冒泡 插入排序

我感觉这个真是没有什么好讲的, 这个是比较简单的, 感觉没有什么必要写一篇博客, 而且这个这么简单的排序问题肯定有人已经有写好的帖子了, 肯定写的比我好, 所以我推荐大家直接去看“左程云”老师的讲解就很好了, 一定是能看懂的, 要是用文字形式再写一遍, 反而有点画蛇添足了…

计算机视觉算法知识详解(含代码示例)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

算法: 二分查找题目练习

文章目录 二分查找二分查找在排序数组中查找元素的第一个和最后一个位置搜索插入位置x 的平方根山脉数组的峰顶索引寻找峰值寻找旋转排序数组中的最小值点名 总结精华模版 二分查找 二分查找 没啥可说的,轻轻松松~ class Solution {public int search(int[] nums, int target…

IDEA 配置 Git 详解

本文将介绍在IntelliJ IDEA 中如何配置Git 没有安装配置 Git 的可以参考我的这篇文章&#xff1a;安装配置 Git 一、操作环境及准备 1.win 10 2.已安装且配置了Git 3.有Gitee账户 4.安装了IntelliJ IDEA 2023.2.1 5.全程联网 二、配置步骤 2.1 配置git 1.采用全局设置&…

Pragmatic Task务实任务——指导语义通信的优化

1. 语义通信 语义通信&#xff08;Semantic Communication&#xff09;的核心理念是传递不仅仅是数据本身&#xff0c;而是数据所包含的“语义”或“意义”。这与传统通信系统不同&#xff0c;传统系统只注重如何准确、高效地传输数据&#xff0c;而语义通信则要求传输的信息能…

基于Pcap4j收发自定义协议报文(注意事项/踩坑总结)

大致内容&#xff1a;完善自定义的Cat21协议&#xff0c;补充至少5个数据类型不同的协议字段 用户输入Cat21协议字段&#xff0c;发送数据包 用户捕获Cat21数据包&#xff0c;打印输出字段值 本篇博客是直接将自定义协议报文封装在MAC帧的payload中的。 一、Cat21Packet类 1…

拓扑排序简介

拓扑排序(Topological Sort)是一种重要的图算法,用于对有向无环图(DAG, Directed Acyclic Graph)中的节点进行排序。拓扑排序的结果是一种线性序列,使得对于图中的任意一条有向边(u, v),顶点u都在顶点v之前。这种排序常用于任务调度、编译器依赖关系分析等领域。 拓…

算法题总结(八)——字符串

531、反转字符串二 给定一个字符串 s 和一个整数 k&#xff0c;从字符串开头算起&#xff0c;每计数至 2k 个字符&#xff0c;就反转这 2k 字符中的前 k 个字符。 如果剩余字符少于 k 个&#xff0c;则将剩余字符全部反转。如果剩余字符小于 2k 但大于或等于 k 个&#xff0c…

VTK有向包围盒

文章目录 一、vtkOBBTree1.1 几种树结构的对比1.2 获取线段与数据集的交点1.3 OBB树可视化1.4 对齐两个数据集1.5 圆柱形有向包围盒 本文的主要内容&#xff1a;简单介绍VTK中有向包围盒的相关功能。 主要涉及vtkOBBTree类。 哪些人适合阅读本文&#xff1a;有一定VTK基础的人。…

python全栈开发是什么?

全栈指掌握多种技能&#xff0c;并能利用多种技能独立完成产品。通俗的说就是与这项技能有关的都会&#xff0c;都能独立完成。 python&#xff0c;因为目前很火&#xff0c;能开发的项目很多。例如&#xff1a;web前端后端&#xff0c;自动化运维&#xff0c;软件、小型游戏开…

基于ssm的二手手机商城的设计与实现

文未可获取一份本项目的java源码和数据库参考。 题目简介&#xff1a; 随着时代的发展与科技的进步&#xff0c;人们的物质生活水平越来愈高&#xff0c;对智能化电子产品的需求也越来越高&#xff0c;手机就是一个很好的表现。近年来&#xff0c;随着华为、小米、vivo、ipho…

问题-python-运行报错-SyntaxError: Non-UTF-8 code starting with ‘\xd5‘ in file 汉字编码问题

​ 编码: 把字符转换成字节序列的过程。因为计算机只能处 理二进制数据&#xff0c;所以不能直接处理文本&#xff0c;需要先把文本转换为二进制数据。 解码: 把二进制数据转换成字符的过程。把接收到的数据转换成程序中使用的编码方式。 ​ 这个报错原因就是编码和解码没达成…

地理定位营销与开源AI智能名片O2O商城小程序的融合与发展

摘要&#xff1a;本文阐述地理定位营销的概念、手段及其在商业中的应用&#xff0c;探讨开源AI智能名片O2O商城小程序如何与地理定位营销相结合&#xff0c;为企业营销带来新的机遇与挑战。 一、引言 在当今数字化营销的时代&#xff0c;地理定位营销已成为一种重要的营销手段…

【C语言】分支与循环

文章目录 前言if语句关系操作符逻辑操作符&#xff1a;&& , || , &#xff01;switch语句if语句和switch语句的对比 while循环for循环do-while循环break和continue语句循环嵌套goto语句 前言 C语⾔是结构化的程序设计语⾔&#xff0c;这⾥的结构指的是顺序结构、选择&…

【GeekBand】C++设计模式笔记5_Observer_观察者模式

1. “组件协作”模式 现代软件专业分工之后的第一个结果是“框架与应用程序的划分”&#xff0c;“组件协作”模式通过晚期绑定&#xff0c;来实现框架与应用程序之间的松耦合&#xff0c;是二者之间协作时常用的模式。典型模式 Template MethodStrategyObserver / Event 2.…