当前位置:   article > 正文

C++11新特性详解

c++11新特性

C++11简介

相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

列表初始化

{}初始化

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

struct Point
{
 	int _x;
 	int _y;
};
int main()
{
 	int array1[] = { 1, 2, 3, 4, 5 };
 	int array2[5] = { 0 };  //0 0 0 0 0 
 	Point p = { 1, 2 };  //运用了struct结构体的语法     初始化
 	return 0; 
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

struct Point
{
	int _x;
	int _y;

	Point(int x, int y)
		:_x(x)
		, _y(y)
	{}
};

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

private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	
	Point p = { 1, 2 };
	//Point p{ 1, 2 };
	
	Point* p3 = new Point[3]{ {1, 1}, { 2, 2 }, { 3, 3 } };

	int* p1 = new int(0);
	int* p2 = new int[5]{1,2,3,4,5};  
	//C++11中 new可以初始化数组

	//自定义类型的列表初始化
	Date d1(2022, 3, 13);
	Date d2 = { 2022, 3, 15 };
	Date d3{ 2022, 3, 15 };
	Date{2022,3,15};

	int i = 1;
	int j = { 2 };
	int k{ 3 };
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

总结:

  • C++11里面扩展了{}初始化使用,基本都可以使用它来初始化
  • 但是建议还是按旧的用法来使用,一般new[]建议使用它来初始化

std::initializer_list

在这里插入图片描述

模拟实现的vector支持{}初始化和赋值:

template<class T>
class vector 
{
public:
	typedef T* iterator;
	vector(initializer_list<T> l)
	{
		_start = new T[l.size()];
		_finish = _start + l.size();
		_endofstorage = _start + l.size();
		iterator vit = _start;
		typename initializer_list<T>::iterator lit = l.begin();
		while (lit != l.end())
		{
			*vit++ = *lit++;
		}
		//for (auto e : l)
		//   *vit++ = e;
	}
	vector<T>& operator=(initializer_list<T> l) {
		vector<T> tmp(l);
		std::swap(_start, tmp._start);
		std::swap(_finish, tmp._finish);
		std::swap(_endofstorage, tmp._endofstorage);
		return *this;
	}
private:
	iterator _start;
	iterator _finish;
	iterator _endofstorage;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

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

class A
{
public:
	A(int a, double d, int b)
		:_a(a)
		, _d(d)
		, _b(b)
	{}
private:
	int _a;
	double _d;
	int _b;
};
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//内置类型的列表初始化
	vector<int> v1 = { 1, 2, 3, 4, 5 };
	vector<int> v2{ 1, 2, 3, 4, 5 };
	//vector (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());


	auto lt1 = {1, 2, 3, 4};
	initializer_list<int> lt2 = { 1, 2, 3, 4 };
	//auto=initializer_list<int>


	map<string, int> dict1 = { pair<string, int>("sort", 1), pair<string, int>("insert", 2) };
	map<string, int> dict2 = { { "sort", 1 }, { "insert", 2 } };
	//首先{ "sort", 1 }, { "insert", 2 }创建成initializer_list<map<string,int>>类型的对象
	//最后map (initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
	//这个构造函数初始化dict2


	//自定义类型的列表初始化
	Date d1(2022, 3, 13);
	Date d2 = { 2022, 3, 15 };
	Date d3{ 2022, 3, 15 };
	Date{ 2022, 3, 15 };


	
	A aa1 = { 1, 1.11, 1};
	list<A> lt = { { 1, 1.11, 1}, { 2, 2.22, 1} };
	//首先{ { 1, 1.11, 1}, { 2, 2.22, 1} }创建成initializer_list<list<A>>类型的对象(首先是A类得有相应的构造函数才行)
	//最后list的构造函数初始化it
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

总结:

  • 一个自定义类型调用{}初始化,本质是调用对应的构造函数
  • 自定义类型对象可以使用{}初始化,必须要要有对应参数类型和个数的构造函数
  • STL容器支持{}初始化,容器支持一个initializer_list作为参数的构造函数

声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

auto(在C++中不支持C语言中原来auto的用法)

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型

int main()
{
	int i = 10;
	auto p = &i;
	
	auto pf = strcpy;
	//输出p、pf的类型
	cout << typeid(p).name() << endl;
	cout << typeid(pf).name() << endl;
	
	map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
	//map<string, string>::iterator it = dict.begin();
	
	auto it = dict.begin();
	
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

注意: typeid(变量、对象).name()得到的结果是字符串,这个是不能 当作类型的

在这里插入图片描述

decltype

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

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret = t1 * t2;
	vector<decltype(t1* t2)> v;
	v.push_back(ret);
	cout << typeid(ret).name() << endl;
}

int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcpy;
	decltype(pf) pf1;    //char * (__cdecl*)(char *,char const *)
	vector<decltype(pf)> v;

	cout << typeid(p).name() << endl;
	cout << typeid(pf).name() << endl;

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

注意:decltype与auto的差别:auto必须要求显式初始化,而decltype没要求

nullptr

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

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

范围for循环

见手撕STL系列

STL中一些变化

在这里插入图片描述
arrary
在这里插入图片描述
注意:

  • a1、a2没什么太大区别,他俩都是从栈上开辟空间的
  • a1越界时会报错(assert断言)、而a2是抽查(越界不一定报错)

forward_list

  • forward_list就是单链表
  • forward_list中支持向后插入(insert_after)、向后删除(erase_after)不支持向前删除和插入(效率太低)
  • forward_list支持单向迭代器

unordered_set、unordered_map
【手撕STL】unordered_set、unordered_map(用哈希表封装)

容器中的一些新方法

  • 基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。
  • 实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:
    在这里插入图片描述
  • 针对旧容器,基本都增加移动构造,移动赋值所有插入数据接口,都增加右值引用版本,这些接口都是用来提高效率的

右值引用和移动语义

注意:图中和代码上所写的移动拷贝改为移动构造

左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,无论左值引用还是右值引用,都是给对象取别名。

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

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;


	//	可以取地址
	cout << &p << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &(*p) << endl;
	
	b = c;
	
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10; 
	x + y;
	fmin(x, y);
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	
	//	// 不能取地址
	//	cout << &10 << endl;
	//	cout << &(x+y) << endl;
	//	cout << &fmin(x, y) << endl;
	
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	//10 = 1; 
	//x + y = 1;
	//fmin(x, y) = 1;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	int& rr = rr1;
	//rr2 = 5.5;  // 报错
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意:左值引用可以连续左值引用,而右值引用是不可以连续右值引用的(右值引用后该变量为左值,不能使用右值引用了)

左值引用与右值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a;// ra为a的别名
	//int& ra2 = 10;// 编译失败,因为10是右值
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
int main()
{
	// 右值引用只能右值,不能引用左值
	int&& r1 = 10;

	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	int a = 10;
	//int&& r2 = a;  //报错 右值引用只能右值,不能引用左值
	// 右值引用可以引用move以后的左值
	int&& r3 = move(a);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

总结:

  • 左值引用可以取地址,左值基本都可以出现赋值符号左边也可以出现赋值符号右边,可以修改,但是const修饰的左值,只能获取地址,不能赋值
  • 右值不能出现赋值符号的左边,只能出现在右边,也就是不能修改,右值不能取地址
  • 本质上引用都是用来减少拷贝,提高效率
    1. 左值引用解决大部分的场景。(做参数、做返回值)
    1. 右值引用是左值引用一些盲区的补充。

在这里插入图片描述

右值引用使用场景和意义

右值引用可以补齐左值引用的短板

namespace lc
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;

			//this->swap(s);
			swap(s);
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

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

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

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

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121

左值引用的使用场景:

做参数和做返回值都可以提高效率。

在这里插入图片描述
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。

在这里插入图片描述

在这里插入图片描述
这两个结果相同无论是否左值引用结果都一样,这是第一个是由于编译器优化的结果,而第二个是正常的

总结:

  • 左值引用,引用传参可以减少拷贝
  • 左值引用,引用返回可以减少拷贝,但是效果不明显

右值引用和移动语义解决上述问题:

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人(将亡值)的资源来构造自己。

在这里插入图片描述

注意:
在这里插入图片描述

注意:出了作用域,如果返回对象不在了,不能使用引用返回(左值引用和右值引用都不可以)

namespace lc
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;
			//this->swap(s);
			swap(s);
		}

		// 拷贝赋值
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

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

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

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

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};



	string to_string(int val)
	{
		string str;
		while (val)
		{
			int i = val % 10;
			str += ('0' + i);
			val /= 10;
		}
		reverse(str.begin(), str.end());
		return str;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152

在这里插入图片描述

不仅仅有移动构造,还有移动赋值:
在这里插入图片描述

注意:

  • 右值:
    1. 内置类型表达式的右值,纯右值。
    1. 自定义类型表达式的右值,将亡值。
    1. 右值是将亡值而将亡值不一定是右值
  • 一个深拷贝的类,除了实现拷贝构造和拷贝赋值还是实现移动构造和移动赋值。面对这个类,函数传值返回的场景,就能进一步减少拷贝,提高效率

总结:

  • 左值引用通常在传参和传返回值的过程减少拷贝,一般直接是利用左值引用的语法特性,别名特性,减少拷贝。
  • 右值引用,一般是利用深拷贝的类,需要实现移动构造和移动赋值,利用移动构造和移动赋值在传参和传返回值过程中简间接转移资源,减少拷贝

STL中的容器都是增加了移动构造和移动赋值、插入接口函数:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

注意:

按照语法,右值引用只能引用右值,但右值引用也能引用左值。因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move 函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
使用move函数时,要注意该对象的资源(所开空间),该对象的资源会根据移动赋值、移动拷贝来决定,建议使用完move函数之后不要再使用该对象了

完美转发

模板中的&& 万能引用

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t) {
	Fun(t);
}
int main()
{
	PerfectForward(10);// 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);// const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

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

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

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t) {
	Fun(std::forward<T>(t));
}
int main()
{
	PerfectForward(10);// 右值
	int a;
	PerfectForward(a);// 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);// const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

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

完美转发实际中的使用场景:

template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};
template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void PushBack(T&& x)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));
	}
	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};
int main()
{
	List<lc::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

在这里插入图片描述

新的类功能

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

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

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。

  • 针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
    1. 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝),自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
    1. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
    1. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
  • 默认生成移动构造和移动赋值条件非常苛刻

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

注意:
以上代码在vs2013中不能体现,vs2013没有很好支持这个特性(vs2013没有载入C++11的一些新增语法、特性),在vs2019下才能演示体现上面的特性。

类成员变量初始化

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化.

再谈类与对象
类的默认成员函数

注意:

  • static 不能给缺省值,必须去类外面定义初始化
  • const static 可以给值初始化(在类内)。并且不是缺省值。

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

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

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	Person(Person&& p) = default;
private:
	lc::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

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

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p) = delete;
private:
	lc::string _name;
	int _age;
};
int main()
{
	Person s1;
	//C2280	“Person::Person(const Person&)”: 
	//Person s2 = s1;    
	//Person s3 = std::move(s1);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

C+±继承

C+±-多态

可变参数模板

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

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

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

在这里插入图片描述

方式一:递归函数方式展开参数包

// 递归终止函数
template <class T>
void ShowList(const T& t) {
	cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args) {
	cout << value << " ";
	ShowList(args...);
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在这里插入图片描述

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

template <class T>
void PrintArg(T t) {
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args) {
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表, 通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…} 将会展开成 ((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ) ,最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]
  • 由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

注意int arr[] = { PrintArg(args)... };也可以使用这个

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

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

emplace_back与push_back用法:

int main()
{
	std::list< std::pair<int, char> > mylist;
	
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	//mylist.emplace_back({ 2, 'v' });  emplace_back不支持这种写法(模板参数,而push_back的参数是固定的可以推导)
	
	std::pair<int, char> kv(100, 'x');
	mylist.emplace_back(kv); // pair 左值
	mylist.emplace_back(make_pair(30, 'c')); // pair 右值
	mylist.push_back(kv);
	mylist.push_back(make_pair(30, 'c'));
	mylist.push_back({ 50, 'e' });

	mylist.emplace_back(10, 'a');

	/*std::pair<int, char> kv1(100, 'x');构造
	std::pair<int, char> kv2 = { 100, 'x' }; 构造+拷贝
	std::pair<int, char> kv3{ 100, 'x' };构造+拷贝
	std::pair<int, char> { 100, 'x' };匿名对象 */

	for (auto e : mylist)
		cout << e.first << ":" << e.second << endl;

	return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

emplace_back与push_back的区别

int main()
{

	std::list< std::pair<int, lc::string> > mylist;

	std::pair<int, lc::string> kv(1, "11111");
	mylist.push_back(kv);
	mylist.emplace_back(kv);
	cout << endl;

	mylist.push_back(make_pair(2, "sort"));
	mylist.push_back({ 40, "sort" });

	cout << endl;
	mylist.emplace_back(make_pair(2, "sort"));
	mylist.emplace_back(10, "sort");

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在这里插入图片描述

总结:

  • emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象,那么除了用法上,和push_back没什么太大的区别
  • 传左值对比 – 没有区别;传右值对比 – push_back是构造+移动构造 emplace_back是直接构造
  • 形态有区别,如果push_back/emplace_back的参数对象及其成员都实现了移动构造,本质区别不大, 因为构造出来+移动构造,和直接构造成本差不多, 但是如果push_back/emplace_back的参数对象及其成员没有实现移动构造,那么emplace_back还是直接构造,push_back则是构造+拷贝构造(没有移动构造编译器会调用拷贝构造),因此代价就大了
  • 稳妥一点呢用emplace_back更好,因为它可以不依赖参数对象是否提供移动构造

注:emplace_back在底层是实现是先构造(new)再调用定位new;push_back先构造(new)再拷贝构造(或者移动拷贝)

lambda表达式

可调用类型-类型定义的对象可以像函数一样去调用

  1. 函数指针bool (*pf)(int, int); --晦涩难懂,不好用
  2. 仿函数―类+重载了operator()–相比函数指针好用多了
  3. lambda表达式
  4. 包装器

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

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

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

#include <algorithm>
#include <functional>
struct Goods
{
	string _name;
	double _price;
};
struct Compare
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price <= gr._price;
	}
};
int main()
{
	Goods gds[] = { { "苹果", 2.1 }, { "香蕉", 3 }, { "橙子", 2.2 }, {"菠萝",
   1.5} };
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare());
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

lambda表达式(可调用类型对象)

struct Goods
{
	string _name;
	double _price;
};
int main()
{
	Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝",
   1.5} };
	sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const
	Goods& r)->bool
		{
			return l._price < r._price;
		});
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

lambda表达式语法

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

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

注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

void(*PF)();
int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};

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

	// 省略了返回值类型,无返回值类型
	auto fun1 = [&a,&b](int c) {b = a + c; };
	fun1(10);
	cout << a << " " << b << endl;



	//捕捉列表可以是lambda表达式
	auto fun = [fun1] {cout << "hello" << endl;};
	fun();

	
	// 各部分都很完善的lambda函数
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;

	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;

	// 编译失败--->提示找不到operator=()  
	//auto fun3 = [&a,&b](int c) {b = a + c;};
	//fun1 = fun3;   

	//允许使用一个lambda表达式拷贝构造一个新的副本
	auto fun3(fun);
	fun();

	//可以将lambda表达式赋值给相同类型的函数指针
	auto f2 = [] {};
	PF = f2;
	PF();
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量

捕获列表说明

  • 捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
    1. [var]:表示值传递方式捕捉变量var
    1. [=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)
    1. [&var]:表示引用传递捕捉变量var
    1. [&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this)

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

函数对象与lambda表达式

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

class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};
int main()
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);
	// lamber
	auto r2 = [=](double monty, int year)->double {return monty * rate * year;
	};
	r2(10000, 2);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

在这里插入图片描述
解析:
处理lambda表达式,在底层看来需要随机生成uuid,生成名叫lambda_uuid类,lambda表达式作为operator()函数的函数体、函数参数、返回值;lambda表达式相当于构造了匿名对象。因此使用是需要用auto(编译器生成的lambda_uuid,我们是看不到的)来生成对象(auto r2 = [=](double monty, int year)->double{return montyrateyear; };)

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

包装器

function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

之前学过的函数指针、仿函数、lamber表达式这些都是可调用的类型!但是类型丰富,无法做到类型统一,可能会导致模板的效率低下!这是就需要包装器了

template<class F, class T> T useF(F f, T x) {
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i) {
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	cout << useF(f, 11.11) << endl;   //count:1 count:0025C140  5.555
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;   //count:1 count: 0025C144 3.70333
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;  //count : 1 count: 0025C148 2.7775
	return 0;
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

通过上面的程序验证,会发现useF函数模板实例化了三份。

包装器用法:

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

解决办法:

template<class F, class T> T useF(F f, T x) {
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	
	return (f(x));
}
double f(double i) {
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};



int main()
{
	// 函数名
	function<double(double)> fn1 = f;
	//function<double(double)> fn1 ( f); 支持
	cout << useF(fn1, 11.11) << endl;   

	// 函数对象
	function<double(double)> fn2=Functor();
	//function<double(double)> fn2(Functor());   //不支持,因为function<double(double)>、Functor()不是同一个类型(类型匹配匹配不上)
	//function<double(Functor,double)> fn2=&Functor::operator();  //支持
	//function<double(double)> fn2 = bind(&Functor::operator(), Functor(), placeholders::_1); //支持
	cout << useF(fn2, 11.11) << endl;  

	// lamber表达式
	function<double(double)> fn3([](double d)->double { return d / 4; });
	cout << useF(fn3, 11.11) << endl;  
	return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

function包装器的使用

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

	// 类的成员函数
	// 包装类的静态成员函数
	std::function<int(int, int)> func4 = Plus::plusi;
	//std::function<int(int, int)> func4 = &Plus::plusi; 都可以
	cout << func4(1, 2) << endl;
	// 包装类的非静态成员函数
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

总结:包装器可以是使用在map、unordered_map……这类容器中便于调用函数(eg:map<string,function<int(int,int)>> m)

bind

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

//头文件<functional>
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。 调用bind的一般形式:auto newCallable =bind(callable,arg_list);`

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

bind的使用

int Plus(int a, int b) {
	return a + b;
}
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
	//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
	// 使用bind进行优化

	std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2);
	//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
	//func2的类型为 function<void(int, int, int)> 与func1类型一样
	//表示绑定函数 plus 的第一,二为: 1, 2
	// 需要绑定的参数,直接绑定值,不需要绑定的参数给 placeholders::_1 、  placeholders::_2.... 进行占位
	function<int()> func2 = bind(Plus, 1,2);

	cout << func1(1, 2) << endl;
	cout << func2() << endl;
	Sub s;
	// 绑定成员函数
	std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,placeholders::_1, placeholders::_2);
	// 参数调换顺序
	std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,placeholders::_2, placeholders::_1);
	cout << func3(1, 2) << endl;
	cout << func4(1, 2) << endl;
	return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

线程库

thread类的简单介绍

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

thread在线文档说明

在这里插入图片描述
注意:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的
    状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include <thread>
int main()
{
	std::thread t1;
	cout << t1.get_id() << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

// vs下查看
typedef struct
{ /* thread identifier for Win32 */
 void *_Hnd; /* Win32 HANDLE */
 unsigned int _Id;
} _Thrd_imp_t;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

thread的使用

int main()
{
	int n = 2;
	//cin >> n;
	vector<thread> works(n);
	std::mutex mtx;
	size_t x = 0;
	size_t total_time = 0;
	for (auto& thd : works)
	{
		thd = thread([&mtx, &x, &total_time](){
			size_t begin = clock();
			mtx.lock();
			for (int i = 0; i < 100000; ++i)
			{
				++x;
			}
			size_t end = clock();
			total_time += (end - begin);
			mtx.unlock();

			cout << this_thread::get_id() << ":" << end - begin << endl;
		});
	}

	for (auto& thd : works)
	{
		thd.join();
	}

	cout << x << endl;
	cout << total_time << endl;

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  1. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
    线程函数一般情况下可按照以下四种方式提供:
  • 函数指针
  • lambda表达式
  • 函数对象
  • 包装器
void ThreadFunc(int a) {
	cout << "Thread1" << a << endl;
}

void T()
{
	cout << "hello" << endl;
}

class TF
{
public:
	void TT()
	{
		cout << "NI" << endl;
	}
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};
int main()
{
	// 线程函数为函数指针
	thread t1(ThreadFunc, 10);

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

	// 线程函数为函数对象
	TF tf;
	thread t3(tf);
	thread t5(&TF::TT, TF());

	//线程函数为包装器
	function<void()> t = T;
	thread t4(t);

	t1.join();
	t2.join();
	t3.join();
	t4.join();
	t5.join();
	cout << "Main thread!" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  1. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

在这里插入图片描述
在这里插入图片描述

  1. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
  • 采用无参构造函数构造的线程对象
  • 线程对象的状态已经转移给其他线程对象
  • 线程已经调用jion或者detach结束

线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数

原子性操作库(atomic)

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

在这里插入图片描述

这是C++98的做法可以对共享修改的数据可以加锁保护。

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

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

在这里插入图片描述

注:

  • 在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
  • 更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
atmoic<T> t;    // 声明一个类型为T的原子类型变量t
  • 1
  • 注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了
#include <atomic>
int main()
{
	atomic<int> a1(0);
	//atomic<int> a2(a1);   // 编译失败
	atomic<int> a2(0);
	//a2 = a1;               // 编译失败
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意:

  • 用printf打印原子类型变量时,在vs2013中可以打印,但是在vs2019中是不可以的
  • 解决办法:
    1. 用cout打印
    1. 强转该变量
    1. 用atomic中的load函数 eg:printf(“%u\n”, total_time.load());
      在这里插入图片描述

补充:
在这里插入图片描述

lock_guard与unique_lock

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

两个线程交替打印1-100,一个线程打印奇数,一个线程打印偶数,交替打印


int main()
{
	mutex mtx;
	int i = 1;

	// 打印奇数
	thread t1([&i, &mtx](){
		while (i < 100)
		{
			mtx.lock();

			// 构造的加锁,lock析构的析构的时候自动解锁
			//std::lock_guard<mutex> lock(mtx);
			//std::unique_lock<mutex> lock(mtx);
			cout << this_thread::get_id() << ":" << i << endl;
			i += 2;

			//std::this_thread::sleep_for(std::chrono::milliseconds(100));//线程睡眠100毫秒
			mtx.unlock();

		}
	});


	// std::this_thread::sleep_for(std::chrono::milliseconds(1));//线程睡眠1毫秒

	int j = 2;
	// 打印偶数
	thread t2([&j, &mtx](){
		while (j < 100)
		{
			mtx.lock();
			//std::lock_guard<mutex> lock(mtx);
			cout << this_thread::get_id() << ":" << j << endl;
			j += 2;
			//std::this_thread::sleep_for(std::chrono::milliseconds(100));
			mtx.unlock();
		}
	});

	t1.join();
	t2.join();

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

解析: 以上代码打印会出现不交替打印的情况,出现这类的情况:

  • 极端场景下:主线程执行到这里(打印偶数)时间片用完了,进入休眠排队
  • 假设某次unlock以后,打印奇数的时间片到了,进入休眠排队
  • 极端情况下,建立线程t1之后开始打印奇数,打印完之后,主线程还没建立好t2线程

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

mutex的种类

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

  1. std::mutex
  • C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
    在这里插入图片描述
    注意,线程函数调用lock()时,可能会发生以下三种情况:
  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

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

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
  1. std::recursive_mutex
    其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。
  2. std::timed_mutex
    比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。
  • try_lock_for()
    接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
  • try_lock_until()
    接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
  1. std::recursive_timed_mutex

lock_guard

std::lock_gurad 是 C++11 中定义的模板类。定义如下:
在这里插入图片描述
通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

unique_lock

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

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

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)

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

在这里插入图片描述

补充:

  • 当调用wait之前,lck一定是被锁住的; 当调用wait时,lck是被解锁了
  • 当进程阻塞时,可以使用notify_one()函数解阻塞
  • void wait (unique_lock& lck, Predicate pred);就是将void wait (unique_lock& lck);封装起来了
  • notify_one()是将其中一个阻塞进程唤醒(随机的)
int main()
{
	mutex mtx;
	condition_variable cv;
	bool flag = true;

	int j = 2;
	// 打印偶数
	thread t2([&j, &mtx, &cv, &flag](){
		while (j < 100)
		{
			std::unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag](){return !flag; });
			
			cout << this_thread::get_id() << ":" << j << endl;
			j += 2;

			flag = true;

			cv.notify_one();
		}
	});

	int i = 1;
	// 打印奇数
	thread t1([&i, &mtx, &cv, &flag](){
		while (i < 100)
		{
			std::unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag](){return flag; });

			cout << this_thread::get_id() << ":" << i << endl;
			i += 2;
			flag = false;

			cv.notify_one();
		}
	});



	t1.join();
	t2.join();

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

总结:

  • 本质是通过flag和条件变量配合控制互斥
  • 当一个打印运行时,另一个肯足没有打印运行( 可能有3种状态)
    1. 时间片用完,休眠排队
    1. wait
    1. lock

注意:用两个条件变量来实现时,要使用两把锁

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/573753
推荐阅读
相关标签
  

闽ICP备14008679号