C++11: 右值引用,移动语义,万能引用,完美转发,新的默认成员函数

时间:2024-04-11 13:50:56

C++11: 右值引用,移动语义,万能引用,完美转发,新的默认成员函数

  • 一.左值和右值
    • 1.左值
    • 2.右值
    • 3.左值,右值和能否被修改的关系
  • 二.左值引用的好处和局限
    • 1.完全解决了传值传参时的深拷贝问题
    • 2.传引用返回时需要注意的点
      • 1.坑点:传引用返回用值接收
      • 2.传引用返回用引用接收
      • 3.应该怎么办?
      • 4.如果传值返回拿引用接收呢?
        • 1.左值引用与右值的关系
    • 3.传值返回时的深拷贝问题
      • 1.编译器未优化的情况
      • 2.编译器优化的情况
  • 三.右值引用与移动语义
    • 1.为什么要有右值引用?
    • 2.移动构造
      • 1.移动构造的价值所在
      • 2.纯右值和将亡值
      • 3.为什么const左值引用能够引用右值
    • 3.移动赋值
    • 4.右值引用与左值的关系
  • 四.STL库的变化
  • 五.改造list的insert,使其支持右值引用类型
    • 1.补充
      • 1.为什么右值引用本身是左值呢?
      • 2.const右值引用的价值是什么
      • 3.右值引用的底层与右值引用的修改
    • 2.改造
    • 3.验证
  • 六.右值引用使用时的注意事项
  • 七.万能引用
  • 八.完美转发
  • 九.新的默认成员函数

C++11引入了右值引用的语法之后
许多大佬就利用右值引用设计出了移动语义,而随着移动语义的出现与普及,为了方便移动语义的使用,随之产生了完美转发这一语法

因此不要把右值引用和移动语义混为一谈
移动语义是右值引用的一大重要应用,而不是右值引用本身

一.左值和右值

在C语言当中,左值和右值这两个概念就早已存在
要真正了解右值引用,就必须要先了解左值和右值

1.左值

  1. 左值并不是单纯指的可以被修改的变量/表达式,而是可以被取地址的变量/表达式
  2. 只要一个变量/表达式能被取地址,该变量/表达式就是左值
  3. 左值既可以出现在赋值符号的左边,也可以出现在赋值符号的右边
  4. const修饰的左值,不能被修改,但是可以取地址
  5. 我们之前所学的引用都是左值引用,符号为&
int main()
{
	int a = 1;
	int* p = new int(1);
	cout << &a << endl;
	cout << &p << endl;
	cout << &*p << " " << p << endl;//p就是*p取地址 p==&*p

	//a,*p,p都是可以被修改的左值
	a = 10;
	*p = 10;
	p = nullptr;

	const int c = 1;
	cout << &c << endl;//c可以被取地址,因此c是左值,只不过因为c被const所修饰,因此c不可被修改
	//c = 10;//c是不可被修改的左值
	return 0;
}

2.右值

  1. 右值是不能被取地址的值
  2. 包括:字面常量0,1,2,3… 函数返回值(返回值类型不是左值引用时) 匿名对象(因为匿名对象的生命周期只有一行,而且没有名字,也就没有地址)等等
  3. 右值不能出现在赋值符号左边,只能出现在赋值符号的右边
  4. 右值不能被修改
  5. 我们今天所学的右值引用的符号为&&
    在这里插入图片描述

3.左值,右值和能否被修改的关系

在这里插入图片描述

二.左值引用的好处和局限

如果大家对左值引用不是很熟悉的话,可以看一下我的这篇文章:
C++入门-引用
在了解了左值和右值之后,我们来回顾一下左值引用的好处和局限
然后就能理解为什么C++11要引入右值引用这一语法
在这里插入图片描述

1.完全解决了传值传参时的深拷贝问题

class A
{
public:
	A()
	{
		_v.resize(10000);
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
private:
	vector<int> _v;
};

void func1(A a)
{
	cout << "void func1(A a)" << endl;
}

void func2(A& a)
{
	cout << "void func1(A a)" << endl;
}

int main()
{
	A a;
	func1(a);
	cout << "================================" << endl;
	func2(a);
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
有了左值引用作为函数的参数
如果你不需要在函数内修改该形参,那么建议传引用作为参数
如果你需要在函数内修改该形参,且想要保证实参不被改变,那么老老实实传值吧
因为这样的需求使得你无论如何都需要拷贝一份

2.传引用返回时需要注意的点

需要注意的是:
传引用返回时必须保证返回值在出了函数作用域之后生命周期还没有结束才可以
否则就是坑人
我们先来看一下坑人的点

1.坑点:传引用返回用值接收

在这里插入图片描述
调试走一波
在这里插入图片描述

2.传引用返回用引用接收

如果说上面的行为在编程界是违反行为,那下面的就是妥妥的犯罪行为了,因为这个进程的肆意妄为,导致OS都要介入了
在这里插入图片描述

3.应该怎么办?

只有当你这个局部变量除了作用域还在,此时可以传引用返回,用引用接收或者用值接收(看你的需求)

比如说静态变量/全局变量的生命周期是全局的,一直存在到程序运行结束
或者是堆上的数据也可以
在这里插入图片描述
因此引用作为返回值时只能解决一部分问题而已,
对于局部非静态变量来说,不能使用传引用返回,否则轻则违法,重则犯罪(语法的法)

4.如果传值返回拿引用接收呢?

我们发现了2个现象

1.左值引用与右值的关系

1.左值引用一般来说不能引用右值
证明:
在这里插入图片描述
2.const左值引用可以引用右值
在这里插入图片描述

3.传值返回时的深拷贝问题

1.编译器未优化的情况

在这里插入图片描述

2.编译器优化的情况

在这里插入图片描述
在这里插入图片描述
这是编译器优化的情况,这里用的VS2019
在这里插入图片描述
我们可以看出
早在C++11之前,编译器就进行了优化,但是还不够,因为语法的限制,有些事情他想做但是做不到

三.右值引用与移动语义

1.为什么要有右值引用?

为了解决局部静态变量无法传引用返回,而只能传值返回造成的资源浪费问题,编译器做了优化,将2次拷贝构造优化为了1次拷贝构造

但是因为C++是追求效率的语言,所以很多大佬就在想能不能让语法开个绿灯,让我们能够将刚才的tmp的资源转移给ret1呢?

反正tmp出了作用域就销毁了,那些资源他也不用,我们帮他用了,这不就是提高资源的利用率吗,而且还避免了浪费,多好啊

正因如此,C++11语法就引入了右值引用,开了绿灯

这个绿灯如何开的呢?
右值引用到底有什么神奇之处呢?
我们以我们之前写过的string类来作为示例一起探索一下右值引用的神奇之处和大佬们的天才设计

源码在这篇博客当中,我就不cv过来了,因为太长了
C++: string的模拟实现
为了方便演示,拷贝构造和赋值运算符重载我们不用现代写法,用传统写法

这是右值引用出现之前,在编译器优化之下的拷贝情况
在这里插入图片描述

2.移动构造

右值引用出现之后,所有的传值返回(我们只谈自定义类型)的返回值(我们姑且统一叫作tmp)
都会被编译器识别为右值进行处理

此时为了让tmp的资源成功转移到接收值(我们姑且统一叫作ret)当中,大佬们根据右值引用和编译器的特殊识别处理设计出了移动构造(是拷贝构造的一种重载形式)
上代码:

string::string(string&& s)
{
	cout << "string::string(string&& s) 移动构造" << endl;
	swap(s);
}

在这里插入图片描述
下面我们调试走一波
在这里插入图片描述
在这里插入图片描述
因此,移动构造的出现是C++委员会,编译器,库的编写者三者齐心协力完成的

右值引用的出现使得右值可以被起别名,同时编译器对传值返回的tmp进行特殊识别处理,将其识别为右值,
此时库的编写者就对拷贝构造写了一个重载版本,专门将右值的资源转移给要构造的对象,从而将拷贝次数减为了0

此时就完全解决了传值返回时的深拷贝问题
不过还有几点我们也要仔细介绍

1.移动构造的价值所在

在这里插入图片描述

2.纯右值和将亡值

在这里插入图片描述

3.为什么const左值引用能够引用右值

在这里插入图片描述

3.移动赋值

可是如果我们把它写成2行呢?
此时就不是构造了,而是赋值了,因此情况就发生了变化
在这里插入图片描述
在这里插入图片描述
此时发生了一次移动构造和一次拷贝赋值重载
刚才刚把那个拷贝优化掉,现在又来了一个拷贝
怎么办?照猫画虎再写一个移动赋值就ok了

string& string::operator=(string&& s)
{
	if (this != &s)
	{
		cout << "string& string::operator=(string&& s) 移动赋值" << endl;
		swap(s);
	}
	return *this;
}

在这里插入图片描述
搞定

4.右值引用与左值的关系

右值引用不能引用左值,但是可以引用move(左值)
move(左值)的返回值类型是一个右值
而move(左值)之后,该左值依然是左值,并不会变成右值

注意:

  1. move(左值)就是让编译器把该左值识别为右值,意味着这个左值的资源有可能会被转移走哦
  2. 慎用move,除非你做好了该左值的资源可能会被转移走的准备

在这里插入图片描述
其实所谓编译器会将传值返回时的左值识别为右值的方法
就是return move(tmp);
但是这个move到底为什么这么厉害呢?
现阶段不建议深入研究move,语法过于复杂
在这里插入图片描述
为什么编译器要帮我们隐含的加上move呢?
因为
编译器要向前兼容,C++11之前的程序也要让他们既能够使用之前的拷贝构造,也能够使用现在的移动语义,使用时无需加上move
否则想要使用移动语义还要把所有传值返回的地方都加上move,那太麻烦了吧

而且他也怕我们忘了加

四.STL库的变化

自从C++11引入了右值引用之后,整个STL库的所有容器都添加了移动语义和右值引用版本的插入
在这里插入图片描述
我们来看一下
在这里插入图片描述

五.改造list的insert,使其支持右值引用类型

在这之前,我们要先补充一个知识点,要不然不好改造
在这里插入图片描述
那么右值引用本身是左值还是右值呢?
有const左值引用,那么有const右值引用吗?

1.补充

在这里插入图片描述
我们可以看出右值引用本身可以取地址,说明右值引用本身是左值
而且的确存在const右值引用

1.为什么右值引用本身是左值呢?

我们可以反过来想,如果右值引用是右值,而又因为右值不可被修改
所以移动构造和移动赋值当中swap根本就调用不了
就算能调用,也没法修改右值啊,所以右值引用不能是右值,而必须要是左值
在这里插入图片描述

2.const右值引用的价值是什么

我们知道const修饰一个变量限制该变量不能被修改
而且右值引用本身是左值,也就意味着右值引用所引用的右值本身可以被修改(因为引用就是取别名,无论你是左值引用还是右值引用)
因此我们就想通了,const右值引用就是限制右值引用本身/右值引用所引用的右值本身不能被修改
在这里插入图片描述
没毛病

3.右值引用的底层与右值引用的修改

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

2.改造

list的模拟实现的源代码在里面,太长了,我就不放过来了
C++ list模拟实现
在这里插入图片描述

void push_back(T&& x)
{
	insert(end(), move(x));
}
iterator insert(iterator pos, T&& x)
{
	Node* newnode = new Node(move(x));
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	newnode->_prev = prev;
	prev->_next = newnode;
	++_size;
	return iterator(newnode);
}
list_node(T&& x)
	:_data(move(x))
	, _next(nullptr)
	, _prev(nullptr)
{}

其他的push_front我们就不加了,因为我们只是试一下来更好的了解库里面如何做的而已

3.验证

在这里插入图片描述
在这里插入图片描述
C++11为了方便我们进行使用,引入了万能引用和完美转发的语法

六.右值引用使用时的注意事项

  1. 右值引用作为参数时大部分情况下都不要加const,因为只要使用右值引用作为参数,肯定就是要从这个右值身上转移点资源,你用const去修饰那不前后矛盾吗
  2. 右值引用一般不作为返回值类型
    给左值引用一样,右值引用作为返回值类型时也会有违法(导致无法拿到想要的结果)和犯罪(野指针/野引用)两种可能性

对于自定义类型:
在这里插入图片描述
对于内置类型
在这里插入图片描述
在这里插入图片描述
此时跟左值引用返回是一样的情况

七.万能引用

万能引用:既能接收左值,也能接收右值,也能接受const左值和const右值
万能引用会根据传入的实参类型进行推导
传入左值,那么t就是左值引用类型
传入右值,那么t就是右值引用类型

template <typename T>
void PerfectForward(T&& t)
{}
//万能引用
template <typename T>
void PerfectForward(T&& t)
{}
int main()
{
	int x = 1;

	PerfectForward(x);//左值
	PerfectForward(10);//右值
	PerfectForward(move(x));//右值

	const int y = 20;
	PerfectForward(y);//const左值
	PerfectForward(move(y));//const右值
	return 0;
}

在这里插入图片描述
在这里插入图片描述
如果没有万能引用的话,针对于刚才的情况,我们就要写4个函数模板:
在这里插入图片描述
行是行,可是没有必要啊,因为函数模板的需要编译器自己推演的
因此C++11委员会就想着,反正都是编译器做,那就让编译器支持一下,这4个版本我们只保留右值引用的那个版本,不过这个T&&并不单指右值引用,而是万能引用

传什么值什么类型我就是什么值什么类型的引用
因此被称为万能引用

八.完美转发

有了万能引用之后,有个场景就尴尬了
因为右值引用的属性是左值,因此想要让它以右值的属性传递下去就需要move一下
而左值引用的属性也是左值,move之后也会变成右值,可是左值引用就不该被move啊

因此这个场景就很难解决,因此C++11委员会在提出万能引用的同时提出了完美转发

作用是:在传参的时候保持右值引用原有的右值属性
同时保持左值引用本身的左值属性

void Func(int& x)
{
	cout << "左值引用" << endl;
}
void Func(const int& x)
{
	cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
	cout << "右值引用" << endl;
}
void Func(const int&& x)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
	Func(std::forward<T>(t));
}
int main()
{
	int x = 1;

	PerfectForward(x);//左值
	PerfectForward(10);//右值,右值引用再传递时属性是左值
	PerfectForward(move(x));//右值

	const int y = 20;
	PerfectForward(y);//const左值
	PerfectForward(move(y));//const右值
	return 0;
}

在这里插入图片描述

九.新的默认成员函数

在这里插入图片描述
为什么条件这么苛刻呢?
我们发现,凡是需要我们自己实现析构,拷贝构造,赋值运算符重载的类肯定是要进行深拷贝的类,
也就是说这三个成员函数是绑定实现的
因此条件这么苛刻也完全正确

//Student类没有移动构造,移动赋值,析构,拷贝构造,赋值运算符重载
//编译器会默认生成移动构造,移动赋值
class Student
{
public:
	Student(const char* name="wzs", int age=20)
		:_name(name)
		,_age(age)
	{}
private:
	wzs::string _name;//调用wzs命名空间中的string的移动构造和移动赋值
	int _age;
};

int main()
{
	Student s1;
	Student s2 = s1;//s1是左值,调用拷贝构造
	Student s3 = move(s1);//move(s1)是右值,调用编译器默认生成的移动构造
	Student s4;
	s4 = move(s2);//move(s2)是右值,调用编译器默认生成的移动赋值
	return 0;
}

在这里插入图片描述

以上就是C++11: 右值引用,移动语义,万能引用,完美转发,新的默认成员函数
的全部内容,希望能对大家有所帮助!!