C++11新增特性:列表初始化&左值引用&右值引用&万能引用&移动构造&移动赋值&引用折叠&完美转发

Hello大家好! 很高兴与大家见面! 给生活添点快乐,开始今天的编程之路。

我的博客: <但愿.

我的专栏: C语言题目精讲算法与数据结构C++

欢迎点赞,关注

目录

一发展历史

二C++11新增统一初始化方式

2.1在C++11之前,{}一般被用于数组或结构体元素的统一的列表初始化值设定。

2.2 C++11扩大的初始化列表的使用范围,可用于所有的内置类型自定义类型的初始化(所有类型)

2.2.1C++11初始化列表的特点(规则)

2.2.2C++11初始化列表的实例

2.3初始化列表初始化容器的底层原理std::initializer_list

2.3.1C++11中的std::initializer_list

2.3.2{}初始化的两种语法使用构造函数实现、使用std::initializer_list实现

2.3.3{}初始化容器的示例

2.3.4 {}初始化容器走给过程(原理)

三 左值/右值及引用

3.1左值和右值

3.1.1左值

3.1.2 右值

3.2 左值引⽤和右值引⽤

3.2.1 左值引用

3.2.2 右值引用

3.3 为什么左值可以取地址,而右值不能

3.4 延长生命周期

3.5 左值和右值的参数匹配

四 右值引⽤的移动语义

4.1左值引⽤的局限性

4.2移动构造和移动赋值(严格上的两个默认成员函数)

4.2.1移动构造和移动赋值的概念

4.2.2移动构造和移动赋值的实现

4.2.2.1移动构造和移动赋值实现的原理

4.2.2.2从移动构造的原理上我们会发现几个问题

五 引⽤折叠

5.1引用折叠的定义

5.2引用折叠的规则

5.3万能引用

六 完美转发

6.1完美转发的作用

6.2完美转发的底层

6.3完美转发和move的区别

一 发展历史

上面这张图的C++版本字体大小就是表示该版本的重要性(版本大小),在C++98开始C++委员会就有五年计划(每五年就制定一个版本), 所以在c++06一个出一个版本但是此时C++已经开始烂尾了(制定不出一个有用的版本),多少到了C++11有没有令人失望(制定了一个大版本)C++06-c++11之间的一个烂尾版本人们叫"C++0x",后C++委员会改成了三年计划(可能被网友骂了,后面就实施****三年计划有什么发布什么)。

C++14没有更新什么东西,C++17中等(很多特性可有可无),因为C++14是在C++11之后,而C++11是一个大版本,而一个版本的分布一定会有问题,一般后面的版本是前一个版本的修订,所以C++14是C++11的修订版本(一般大版本后面又一个小版本【是大版本的修订】)。

【现代公司中长所有的版本】

现在公司中支持比较新的版本可能也就到C++20,C++23支持的还比较少(编译器支持的还不是很好,只有最新的gcc和clang版本编译器还行。记住C++委员会是制定语法规则和库的,但是是由编译器实现落地(一些语法无法实现编译器就不会支持)。

就算编译器,但是一个版本的出现必然会出现一些问题,所以这里对于编译器刚支持一个新语法具有探索性,可能刚开始是支持的不会的(稳定性不够)【因为一个C++新版本的出现,一般都会又问题,所以编译器对于新版本的支持一般会延缓今年(对于一个大版本之后的小版本支持很快,因为大版本之后的小版本是大版本的修订一般没有什么问题)】。

【总结】

在C++98之前那时候语言还很少,此时C++作为老大哥的存在,此时就保存了自己的风格,但是到了C++11由于中间烂了很多年,此时语法设计已经跟不上(在语法的便利性已经有些跟补上了,此时C++委员会就开始抄作业【可能说的难听了一些】,此时C++11就有点飘了不像传统的C++语法。C++98更倾向于C语言的风格C++11之后就变了,所以我们学习C++11之后要像学习新语言一样学习C++,例如C++11的auto就一定也不像C++98的语法(从python中抄过来的)。我们一般重点掌握C++98和C++11即可,现在公司基本上使用这些版本(因为编译器的稳定性决定,老版本的编译器的稳定性更高),最多也就学到C++20(其他版本有余力可以学习,如果后续公司要用的通过查询库+AI即可了解)

二 C++11新增统一初始化方式

2.1在C++11之前,{}一般被用于数组或结构体元素的统一的列表初始化值设定。

cpp 复制代码
struct Point
{
	int _x;
	int _y;
};

int main()
{
	//c++98只支持,对于结构体,数组使用{}初始化。
	int a1[] = { 1,2,3,3 };
	int a2[5] = { 5 };

	Point p = { 1,2 };
}
复制代码

2.2 C++11扩大的初始化列表的使用范围,可用于所有的内置类型自定义类型的初始化(所有类型)

2.2.1C++11初始化列表的特点(规则)

• C++11以后统⼀初始化⽅式,实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化 了以后变成直接构造。
{ }初始化的过程中,可以省略掉= 【注意一定是{ }才可以省略=】
C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便 利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便

2.2.2C++11初始化列表的实例

cpp 复制代码
struct Point
{
	int _x;
	int _y;
};

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

	Date(const Date& d)
		:_year(d._year)
		,_month(d._month)
		,_day(d._year)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	//C++11不管是什么类型都支持使用{}初始化
	//内置类型
	int x0 = 1;
	int x1 = { 3 };
	int x2{ 5 };//注意只有使用{}初始化才可以省略=
	//int x2 2;

	//对于自定义类型也支持
	Date d0(2025, 5, 19);//只是通过构造定义一个日期类对象

	
	Date d1 = { 2025,3,29 };//这不就是前面的多参数隐式类型转化,构造+拷贝构造优化为直接构造
	const Date& d4 = { 2024,5,1 };//这里必须使用const修饰,也间接说明了这里是构造+拷贝构造优化为直接构造
	//而这里引用的是构造产生的临时对象。

	Date d2{ 2025,4,39 };//省略=
	//Date d3 = 2025;
	Date d3 = { 2025 };//由于这里我们给了缺省值,使用支持给一个。

	//{}初始化的使用
	// 上面其实不是我们的使用场景,	上面的使用方法其实没有很方便(相比之前没有方便很多)

	//使用场景-在vector<Date>中插入一个Date对象,
	//我们不用先创建一个Date对象,而是使用{}直接初始化。
	vector<Date> v;
	v.push_back(d1);
	v.push_back(Date(2025, 1, 1));

	// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
	v.push_back({ 2025,3,29 });
	return 0;
}

在C++11中初始化列表可用于容器默认构造函数和拷贝复制函数的统一初始化值的设定

【部分容器示例】

cpp 复制代码
int main()
{

//构造函数的初始化值设定
vector<int> v{1,2,3,4,5};
list<int> l{1,2,3,4,5};
map<string,string> m{
                    {"排序","sort"},
                    {"快排","qsort"}
  };


//拷贝复制函数的初始化设定
vector<int> v1;
v1={1,2,3,4,6};

list<int> l1;
l1={1,2,5,6,7,8};
return 0;
}

2.3初始化列表初始化容器的底层原理std::initializer_list

2.3.1C++11中的std::initializer_list

std::initializer_list文档简介

从库中可以知道initializer_list是一个模板,支持任意多个初始化的原理是,把{ }这个数组中的数据拷贝过来(在栈上,可以通过打印储存在普通位置的数据变量的地址进行验证),这个对象的成员变量只有两个指针first(指向开始位置)、list(指向结尾位置),所以列表中传多少个数据都行,通过两个指针拿到对应的数据插入容器即可。

【在C++11后对于stl中的容器支持{}初始化的原因是因为几乎所有的容器都增加了std::initializer_list作为参数的构造函数和赋值重载函数,使得初始化更方便】

以vector为例

2.3.2{}初始化的两种语法使用构造函数实现、使用std::initializer_list实现

2.3.3{}初始化容器的示例

cpp 复制代码
int main()
{
	//1依赖于该类型的构造函数实现。
	//这里支持1~3个参数初始化,因为Date类型三给成员变量,而其构造函数给每个成员变量都给了缺省值
	Date d1{ 2025};
	Date d2{ 2025,5};
	Date d3{ 2025,5,26 };

	//2依赖于库中的std::initializer_list
	//支持任意多个初始化
	vector<int> v1{ 1,2,3,4,5 };
	vector<int> v2{ 1,3,4,5,6,4,5,78,99,9};
	
	//怎么支持任意多个初始化,如果像Date类一样,要写一个支持任意多个参数的全缺省构造,这里是写不出来的
	// 这里依赖于库中的std::initializer_list,而std::initializer_list,中只有两个指针first,last,所以传多少个都行
	//两个指针分别指向数组的开始和结束,后将数据依次插入。

	//验证实现支持任意多个初始化依赖于库中的std::initializer_list
	auto il1 = { 10,20,12 };
	initializer_list<int> il2 = { 10,20,30,5,2 };
	cout << typeid(il1).name() << endl;//输出il1的类型

	vector<Date> v3 = { {2025,5,19},{2025,5,29},{2025,6,19} };
	vector<Date>{d1, d2, d3};
	// 这里是pair对象的{}初始化和map的initializer_list构造结合到一起用了
	map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };

	const vector<int>& v4 = { 1,2,3,4,5 };
	// initializer_list版本的赋值支持
	v1 = { 10,20,30,40,50 };

	// STL的容器都支持initializer_list版本的构造
	return 0;
}

2.3.4 {}初始化容器走给过程(原理)

三 左值/右值及引用

3.1左值和右值

3.1.1左值

左值 是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址****(判断是否为左值的最根本标准)左值可以出现赋值符号的两边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址

【示例】

cpp 复制代码
int main()
{
	// 左值:可以取地址(区分右值的本质方法)
	//常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("111111");
	s[0] = 'x';
	cout << &c << endl;
	cout << (void*)&s[0] << endl;
}

【不要从名字上理解,不是运算符的左边就是左值,在运算符的右边就是右值】

左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
•右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址【常见的右值有字面量常量、临时对象(原声内置类型的表达式求值,函数传值返回)、匿名对象]。
•区分左值右值的本质是是否能取地址,而能不能取地址在不验证我们是不确定的,所以我们要记住那些是左值那些是右值,常见的右值有字面量常量、临时对象(表达式求值产生的临时对象、函数传值返回产生的临时对象)、匿名对象,其余基本是左值。

3.1.2 右值

右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值只能出现在赋值符号的右边右值不能取地址 (判断是否为右值的最根本标准)

常见的右值:字面量常量临时对象(原声内置类型的表达式求值,函数传值返回)、匿名对象。

【示例】

cpp 复制代码
int main()
{

	// 右值:不能取地址
	double x = 1.1, y = 2.2;
	// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
	10;//字面常量
	x + y;//临时对象-表达式求值返回的临时对象
	fmin(x, y);//临时对象-函数传值返回的临时对象
	string("11111");//匿名对象
	//cout << &10 << endl;
	//cout << &(x+y) << endl;
	//cout << &(fmin(x, y)) << endl;
	//cout << &string("11111") << endl;

	return 0;
}

【总结】

【不要从名字上理解,不是运算符的左边就是左值,在运算符的右边就是右值】

左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的两边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
•右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值只能出现在赋值符号的右边,右值不能取地址【常见的右值有字面量常量、临时对象(原声内置类型的表达式求值,函数传值返回)、匿名对象]。
•区分左值右值的本质是是否能取地址,而能不能取地址在不验证我们是不确定的,所以我们要记住那些是左值那些是右值,常见的右值有字面量常量、临时对象(表达式求值产生的临时对象、函数传值返回产生的临时对象)、匿名对象,其余基本是左值。

3.2 左值引⽤和右值引⽤

• Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别
名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
•左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值 【这也是前面我们没学习右值引用的时候在定义函数参数时定义成const修饰,就是为了实现不管时左值还是右值都能调用】那为什么在右值也可以调用const左值引用的情况下还要实现右值引用的版本(后面会解析)】
•右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)【move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换(也可以理解为它允许这个变量在属性上的转化即左值变成右值,move(s1)但是注意move未改变s1的属性,只是把它强转了],当然他还涉及⼀些引⽤折叠的知识,这个我们后⾯会细讲】
• 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值(后面会解析其作用)
• 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。但是底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。
• template <class T> typename remove_reference<T>::type&& move (T&& arg);

3.2.1 左值引用

前面我们写的引用是C++98的C++语法中引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。

Type& r1 = x; 左值引用。

普通的左值引用只能引⽤左值不能直接引⽤右值。

但是const左值引⽤可以引⽤右值 【这也是前面我们没学习右值引用的时候在定义函数参数时定义成const修饰,就是为了实现不管时左值还是右值都能调用】那为什么在右值也可以调用const左值引用的情况下还要实现右值引用的版本(后面会解析)】

【示例】

cpp 复制代码
int main()
{
	int a = 10;

	int& ra = a;
	//int& rb = 10;//error,普通左值引用不能引用右值
	const int& rc = a;
	const int& rd = 10;
	return 0;
}

3.2.2 右值引用

• Type&& rr1 = y 右值引用。

右值引用只能引⽤右值不能直接引⽤左值。

右值引⽤可以引⽤move(左值)【move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换(也可以理解为它允许这个变量在属性上的转化即左值变成右值,move(s1)但是注意move未改变s1的属性,只是把它强转了],当然他还涉及⼀些引⽤折叠的知识,这个我们后⾯会细讲】

需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值(后面会解析其作用)

【示例】

cpp 复制代码
//左值引用和右值引用
int main()
{
	
	int b = 1;
	int&& rr5 = move(b);
	string s("111111");
  
    // int&& rr1 = b; 右值引用不能引用左值
	
	//右值引用-
	//普通右值引用引用右值
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;//右值引用-引用右值-字面量常量
	double&& rr2 = x + y;//右值引用-引用右值-临时对象(表达式求值)
	double&& rr3 = fmin(x, y);//右值引用-引用右值-临时对象(函数传值返回产生的临时对象)
	string&& rr4 = string("1111111");//右值引用-引用右值-匿名对象

    //使用move将左值转化为右值从而进行右值引用
    string&& rr6 = move(s);//move后变成右值
	// move底层本质跟这里类似,就是强转
    string&& rr7 = (string&&)s;//move的原理

	return 0;
}

3.3 为什么左值可以取地址,而右值不能

左值右持久状态(是相对于右值来说),例如一个变量创建在函数体内其生命周期和函数一样,

而临时对象,匿名对象生命周期只在单前一行,所以这里的左值右持久状态(是相对于右值来说)。

左值通常是我们定义出来会分配到栈、堆..的储存空间上,所以左值可以取地址,而右值不一定它可能储存在储存空间上、也可能储存在寄存器上(寄存器无法取地址)、也可能和代码写死(iint i = 10,代码的底层都是转化成对应的指令,而10在其指令上是直接写死的,虽然右值可能储存在储存空间上但是由于其不确定性所以其不能取地址。

3.4 延长生命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。

cpp 复制代码
int main()
{
	std::string s1 = "asdcvfbgh";
	//std::string&& r1 = s1;   s1是一个左值不能使用右值引用引用左值
	
	//std::string& r2 = s1 + s1; 中间会产生临时对象,不使用const无法实现
	const std::string& r2 = s1 + s1;//使用const修饰左值引用延长生命周期;
	//r2 += "sdfgdt";但是const修饰不能修改

	std::string&& r3 = s1 + s1;//使用右值引用可以延长生命周期
	r3 += "sdfbgnhg";//通过非const修饰可以实现修改

	return 0;
}

3.5 左值和右值的参数匹配

【左值引用、const左值引用、右值引用的匹配】

我们只要记住编译器匹配的原则始终是,没有合适的将就吃,右合适自己的符合自己口味的一定会吃合适自己口味的(即如果三个中右值引用的没有此时右值就会将就匹配使用const左值引用,如果三个同时出现,此时右值就会匹配右值引用)。

【匹配原则】

• C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
• C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
• 注意右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪(后面会讲其作用)

cpp 复制代码
void f(int& x)
{
	cout << "左值引用重载 f(" << x << ")" << endl;
}

void f(const int& x)
{
	cout << "const左值引用重载 f(" << x << ")"<< endl;
}

void f(int&& x)
{
	cout << "右值引用重载 f(" << x << ")" << endl;
}
int main()
{
	int i = 1;
	const int ci = 2;
	f(i); // 调用 f(int&)
	f(ci); // 调用 f(const int&)
	f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
	f(std::move(i)); // 调用 f(int&&)
	return 0;
}

【有右值引用】

【无右值引用】

四 右值引⽤的移动语义

4.1左值引⽤的局限性

【示例实现一个杨辉三角】

cpp 复制代码
// C++98这里的传值返回拷贝代价就太大了,C++11之后效率就很高,不用担心效率
//vector<vector<int>> generate(int numRows) {
//	vector<vector<int>> vv(numRows);
//	for (int i = 0; i < numRows; ++i)
//	{
//		vv[i].resize(i + 1, 1);
//	}
//	for (int i = 2; i < numRows; ++i)
//	{
//		for (int j = 1; j < i; ++j)
//		{
//			vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
//		}
//	}
//	return vv;
//}


//C++11之前,得通过输出型参数改善效率-思路挺好可以了解了解
//这里我们不要返回值,而是将返回值的类型作为一个参数参数传入
//即传一个参数里在函数体中修改,你在函数外可以拿到输出这个参数的内容
void generate(int numRows, vector<vector<int>>& vv) {
	//vector<vector<int>> vv(numRows);
	vv.resize(numRows);
	for (int i = 0; i < numRows; ++i)
	{
		vv[i].resize(i + 1, 1);
	}
	for (int i = 2; i < numRows; ++i)
	{
		for (int j = 1; j < i; ++j)
		{
			vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
		}
	}
}

int main()
{
	// C++11之前,如果编译器没优化,这里代价很多
  // C++11之后就不会担心效率
	//vector<vector<int>> ret = generate(10000);
	vector<vector<int>> ret;
	generate(10000, ret);

	return 0;
}

【分析】

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。前面我们讲过对于引用传参基本上是可以的,但是对于引用作为返回值返回的条件是返回对象出来作用域还存不存在(所以这里使用右值引用也不行,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实)为了解决这个问题C++11引入移动构造和移动赋值(右值引用的移动语义)。

【输出型参数】

在C++98之前为了解决函数的返回值是个临时局部对象的问题,其用法是先定义一个输出型参数对象,在将其作为函数参数传入,最后我们在函数体内对其作出的改变在函数体外也可以拿到。

4.2移动构造和移动赋值(严格上的两个默认成员函数)

4.2.1移动构造和移动赋值的概念

移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值【即移动构造和拷贝构造函数形成函数重载,前面的拷贝构造函数的参数用const修饰是因为当时无左右值引用区分,为了同时可以接受左值引用和右值引用,所以当两者同时存在时传右值调用移动构造,传左值调用拷贝构造;当只有const修饰的拷贝构造函数时不管传左值还是右值都是调用拷贝构造】
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤
对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义;而定义像Date这样的浅拷贝的成员变量类,移动构造和移动赋值无意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是是直接将右值对象的资源窃取过来,占为己有 ,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,用别人的资源来构造自己 从提⾼效率 【后面讲怎么实现移动构造和移动赋值极其原理会做解释】

4.2.2移动构造和移动赋值的实现

4.2.2.1移动构造和移动赋值实现的原理

从名字上来看都有移动顾名思义这里应该通过移动(交换)思想实现。对于左值有存储空间的要拷贝进行的是深拷贝所以我们是无法动它的(无法改变这个过程);而对于右值(字面量常量、临时对象、匿名对象),其****中字面量常量只有内置类型才有,而在自定义类型中只可能右临时对象和匿名对象。而临时对象和匿名对象的生命周期只在当前一行(**即都要死了,如果还进行深拷贝,一拷贝完你就死了无意义,既然你都要死了不如把你的东西给我即交换两个的资源), 所以左值叫拷贝构造因为要进行深拷贝我不敢动你只能老老实实去进行深拷贝,而对于右值你马上都要没了,那你等会还要把那些东西带走,不如交换给我(一般是和一个空对象进行交换)移动赋值和移动构造的实现原理一样**。

4.2.2.2从移动构造的原理上我们会发现几个问题

【问题1 move对于左值的强转】

move将一个左值转化为右值后如果进行移动构造可能会把你的资源抢走(而move的对象是一个左值),这就很危险,所以我们要慎用move

【问题2 为什么右值不能改变而在移动构造中能改变呢?】

前面对于左值引用和右值引用讲过,右值是不可移动的但是当它被右值引用引用后右值引用本身的属性是左值(这就是为什么要实现这个规则)。

【示例:移动构造和拷贝构造函数实现和对比】

cpp 复制代码
namespace wzystring
{
	class string
	{
	public:
		string(const char* str = "")//默认构造函数
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(const char* str = "") ----- 构造函数" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝构造函数
		string(const string& s)
		{
			cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		//移动构造函数
		string(string&& s)
		{
			cout << "string(string&& s) ---- 移动构造函数  移动语义" << endl;
			swap(s);
		}

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
};

int main()
{
	wzystring::string s("123456");
	cout << endl;

	wzystring::string s1 = s;
	cout << endl;

	wzystring::string s2(move(s1));
	return 0;
}

【运行结果】:

在上述过程中,我们发现移动构造过程中没有深拷贝。原因在于移动构造直接将原资源抢占,交换过来。

【示例: 移动赋值函数和拷贝赋值函数实现和对比】

cpp 复制代码
namespace wzystring
{
	class string
	{
	public:
		string(const char* str = "")//默认构造函数
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(const char* str = "") ----- 构造函数" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝赋值函数
		string& operator=(const string& s)
		{
			string tmp(s);
			swap(tmp);
			cout << "string& operator=(const string& s) ---- 拷贝赋值函数 深拷贝" << endl;
			return *this;
		}

		//移动赋值函数
		string& operator=(string&& s)
		{
			swap(s);
			cout << "string& operator=(string&& s) ---- 移动语义" << endl;
			return *this;
		}
		
		//拷贝构造函数
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
			cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
		}

		//移动构造函数
		string(string&& s)
		{
			swap(s);
			cout << "string(string&& s) ---- 移动构造函数  移动语义" << endl;
		}
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
};

int main()
{
	wzystring::string s("123456");
	cout << endl;
	
	wzystring::string s1, s2;
	s1 = s;
	cout << endl;

	s2 = move(s);
	cout << endl;
	return 0;
}

【运行结果】:

五 引⽤折叠

5.1引用折叠的定义

引用折叠 及引用的引用(两个原因分别可以是左值引用和右值引用中的一个),日常我们很少用,因为这种情况我们直接定义是定义不出来的,如 int & && r = i,这样写会直接报错**,只有在模板时才可能实现(通过模板或 typedef 中的类型操作可以构成引⽤的引⽤)**。

5.2引用折叠的规则

一句话总结对于普通的引用只要有一个左值引用 &,结果就是左值引用;只有两个都是右值引用 &&,结果才是右值引用对于其中右一个是const引用只有const右值引用+右值引用才是const右值引用。

【示例】

cpp 复制代码
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{
}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{
}

int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;

//通过typedef中的类型实现折叠引用
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&


// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);

// 没有折叠->实例化为void f2(int&& x)
f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
f2<int&&>(n); // 报错
f2<int&&>(0);

// 折叠->实例化为void f1(const int& x)
f2<const int&>(n);
f2<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f2<const int&&>(n);//报错
f2<const int&&>(0);
return 0;
}

5.3万能引用

从上面可以看出如果模板函数中模板参数是右值引用此时传入的函数参数可以是左值也可以是右值,如果传入的是左值通过折叠引用推导出是左值,我们把这种叫做万能引用(一定要注意是模板函数及有参数类型的推导过程,例如在一个类的成员函数中的一个函数的参数是有值引用,但是其函数类型在类模板实例化就已经确定了所以其不是万能引用)

【示例】

cpp 复制代码
//一定注意是函数模板
//很多类中的成员函数为了支持传右值,此时参数也是右值引用
//但是其函数参数类型在类模板实例化时就已经决定了
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;
	cout << &a << endl;
	cout << &x << endl << endl;
}
int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10); // 右值
	int a;
	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
	Function(a); // 左值
	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值
	const int b = 8;
	// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
	// 所以Function内部会编译报错,x不能++
	Function(b); // const 左值
	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	// 所以Function内部会编译报错,x不能++
	Function(std::move(b)); // const 右值
	return 0;
}

【什么时候使用万能引用,设计目的,过程遇到的问题】

用法:在现实中不想现实实例化,想要其自己推导就使用万能引用。

目的:为了实现万能引用模板(函数模板)在传左值引用时实例化出左值引用;传const 左值引用实例化出const左值引用,在传右值引用实例化出右值引用;传const右值引用实例化出const右值引用。

重点:一定要区分万能函数和右值引用函数前者是一个模板函数有函数参数类型的推导过程,而后者是一个函数可能在类的实例化就已经确定类型了等等。

前面有一个规则就是左值引用的属性是左值;右值引用的属性是左值(右值引用要修改右值对象例如在移动规则中)。那怎么确保在函数传导过程中保证右值引用的属性不变,前面我们区分了左值引用和右值引用可以在右值引用中使用move,但是这里只有万能引用时显然这种方法是不行的,这时候就要引用C++11引入的完美转发来解决这个问题了

六 完美转发

库中的完美转发:std::forward

6.1完美转发的作用

解决传导过程中参数属性的变化问题,可以保持参数原有属性【在万能引用中常用】。即在模板函数里,把传入的实参的值类别(左值 / 右值)和类型原封不动地转发给下一个函数,既不额外拷贝,也不改变参数性质。

依赖于:

  • 万能引用(T&&
  • 引用折叠
  • std::forward

6.2完美转发的底层

底层是个函数模板,写了一个左值和一个右值版本(把属性去掉,及无折叠),通过特化实现。其本质就是根据模板参数T 的类型,把参数强制转回原本的值类别。

  • 如果 T 推导为左值引用 → 返回左值引用
  • 如果 T 推导为非引用 / 右值引用 → 返回右值引用

【示例】

【1 无完美转发】

cpp 复制代码
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<class T>
void Function(T&& t)
{
	// 完美保持他的属性传参
	Fun(t);
}

int main()
{
	// 10是右值,推导出T为int,右值引用的属性是左值所以会调用左值引用
	Function(10); // 左值

	int a;
	// a是左值,推导出T为int&,引用折叠,后变成左值
	Function(a); // 左值

	// std::move(a)是右值,推导出T为int,右值引用的属性是左值所以会调用左值引用
	Function(std::move(a)); // 右值

	const int b = 8;
	// a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
	Function(b);

	// std::move(b)右值,推导出T为const int,const 右值引用的属性是左值所以会调用const 左值引用
	Function(std::move(b)); // const 右值

	return 0;
}

【结果】

【2 有完美转发】

cpp 复制代码
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<class T>
void Function(T&& t)
{
	// 完美保持他的属性传参
	Fun(forward<T>(t));
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10); // 右值

	int a;
	// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
	Function(a); // 左值

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值

	const int b = 8;
	// a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
	Function(b);

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	Function(std::move(b)); // const 右值

	return 0;
}

【结果】

6.3完美转发和move的区别

move:不管什么属性都强转为右值属性

std::forward(完美转发):识别是什么属性,然后强转回对应属性(即保持原有属性,进行产参常配合万能使用。

本篇文章就到此结束,欢迎大家订阅我的专栏,欢迎大家指正,希望有所能帮到读者更好了解C++11知识 ,后面我将继续更新C++11相关知识。觉得有帮助的还请三联支持一下~后续会不断更新算法与数据结构相关知识,我们下期再见。

相关推荐
赫瑞2 小时前
Java中的进制转换
java·开发语言
迷海2 小时前
力扣原题《打家劫舍》递归版动态规划,纯手搓,已验证,未优化
c++·leetcode·动态规划
研來如此3 小时前
C++ 接口设计 && Doxygen 注释
前端·javascript·c++
lsx2024063 小时前
jQuery 删除元素
开发语言
紫金修道10 小时前
【DeepAgent】概述
开发语言·数据库·python
Via_Neo10 小时前
JAVA中以2为底的对数表示方式
java·开发语言
书到用时方恨少!10 小时前
Python multiprocessing 使用指南:突破 GIL 束缚的并行计算利器
开发语言·python·并行·多进程
cch891810 小时前
PHP五大后台框架横向对比
开发语言·php
天真萌泪11 小时前
JS逆向自用
开发语言·javascript·ecmascript