⭐上篇文章:41.C++哈希6(哈希切割/分片/位图/布隆过滤器与海量数据处理场景)-CSDN博客
⭐本篇代码:c++学习/22.C++11新特性的使用 · 橘子真甜/c++-learning-of-yzc - 码云 - 开源中国 (gitee.com)
⭐标⭐是比较重要的部分
目录
一. 右值引用的概念
1.1 左值引用与右值引用
1.2 右值引用引用左值,左值引用引用右值
编辑 1.3 右值与左值在函数参数中的匹配⭐
二. 纯右值与将亡值
三. 移动语义 ⭐
3.1 右值引用做函数参数 - 移动构造
3.2 右值引用做函数参数 - 移动赋值
3.3 右值引用与返回值问题
四. 完美转发 ⭐
4.1 万能引用
4.2 std::forward实现完美转发
五. 引用总结
一. 右值引用的概念
1.1 左值引用与右值引用
引用是给对象取别名,左值引用是给左值取别名,右值引用是给右值取别名。
左值一般为用户定义的可修改的变量,而右值一般是常量,返回值,表达式等临时变量。
一般认为:左值是能够取地址的,而右值无法取地址。
一般使用&&符号去引用右值。如下面代码中的常量10,表达式x+y
#include <iostream>
using namespace std;int main()
{// 1.使用 &去引用左值int a = 10, b = 5;int &a1 = a, &b1 = b;cout << a1 << " " << b1 << endl;// 2.使用&&去引用右值int x = 10, y = 20;int &&x1 = 10;int &&x2 = x + y;cout << x1 << " " << x2 << endl;x1 = 12;x2 = 10;cout << x1 << " " << x2 << endl;return 0;
}
1.2 右值引用引用左值,左值引用引用右值
一般来说,这两种操作都是无法直接实现的。
1.3 右值与左值在函数参数中的匹配⭐
假如有三个重载函数,一个参数为左值引用,一个为const 左值引用,一个为右值引用(没有const 右值引用,因为右值引用过程有资源转移)。
使用这些函数去调用左值/const左值与右值,最终会匹配哪一个函数呢?测试代码如下:
#include <iostream>
using namespace std;template <class T>
void f1(T &a)
{cout << "void f1(T &a)" << endl;
}template <class T>
void f1(const T &a)
{cout << "void f1(const T &a)" << endl;
}template <class T>
void f1(T &&a)
{cout << "void f1(T &&a)" << endl;
}int main()
{int a = 1, b = 2;const int c = 10;f1(a);f1(c);f1(a + b);f1(10);return 0;
}
首先根据代码推测一下运行的结果:第一条应该是T&a,第二条是const T&a,第三条/四条是T&&a
运行结果如下:
可以看到,推测的结果是正确的!
如果我将右值引用的重载函数注释掉呢?理论来说,此时调用右值应该匹配的是const版本。
运行结果如下:
这表示:编译器可以识别右值与左值。
二. 纯右值与将亡值
纯右值是指纯粹的右值:比如常量( 10,"hello" ),非引用类型的返回值( int func() ),表达式(a +b),临时对象。这些值一般无标识,无法取地址。
而将亡值是即将销毁的左值,比如move(左值),右值引用的函数调用。将亡值常用来实现移动语义。
三. 移动语义 ⭐
移动语义指的是,将一个对象的资源直接转移到另一个对象中,减少深拷贝的消耗。通过移动语义来实现右值引用作为参数和右值引用返回,从而达到减少数据的拷贝。
移动语义移动的值是不再使用的值,如传值返回,传入临时对象/右值对象做函数参数。
使用移动语义的时候,一般将函数标记为noexcept,即不会抛出异常。这样编译器就会减少异常处理的代码,并且执行更为激进的操作,比如直接转移资源以提高性能
3.1 右值引用做函数参数 - 移动构造
测试代码如下:分别使用左值引用拷贝构造和右值引用实现移动构造。
#include <iostream>
#include <cstring>
using namespace std;class String
{
public:String(const char *str = ""){if (nullptr == str)str = "";_str = new char[strlen(str) + 1];strcpy(_str, str);}// 右值引用,但是是深拷贝String(const String &s): _str(new char[strlen(s._str) + 1]){cout << " String(const String &s) 深拷贝" << endl;strcpy(_str, s._str);}String(String &&s) noexcept: _str(s._str){cout << " String(String &&s) noexcept 移动拷贝" << endl;s._str = nullptr;}~String(){if (_str)delete[] _str;}private:char *_str;
};String f(const char *str)
{return String(str);
}int main()
{String s("123");cout << " ------------------------------" << endl;String s1(s);cout << " ------------------------------" << endl;// 由于编译器优化,直接转移资源,不调用移动构造cout << " ------------------------------" << endl;String s2(String("临时对象-右值"));cout << " ------------------------------" << endl;cout << " ------------------------------" << endl;String s3(f("临时返回值-右值"));cout << " ------------------------------" << endl;// 通过move强制使用移动构造cout << " ------------------------------" << endl;String s4(move(s1));cout << " ------------------------------" << endl;return 0;
}
测试结果如下:
这样一来,我们就能够避免传入右值对象的时候使用深拷贝这种开销大的操作了。
假如我们将移动构造注释掉,就会发生使用右值对象传参的时候调用深拷贝。
测试如下:
3.2 右值引用做函数参数 - 移动赋值
类中除了拷贝函数,还有重载赋值运算符。此时如果不使用右值引用做参数,并实现移动拷贝。传入右值就会调用开销大的深拷贝!
#include <iostream>
#include <cstring>
using namespace std;class String
{
public:String(const char *str = ""){if (nullptr == str)str = "";_str = new char[strlen(str) + 1];strcpy(_str, str);}// 右值引用,但是是深拷贝String(const String &s): _str(new char[strlen(s._str) + 1]){cout << " String(const String &s) 深拷贝" << endl;strcpy(_str, s._str);}String(String &&s) noexcept: _str(s._str){cout << " String(String &&s) noexcept 移动拷贝" << endl;s._str = nullptr;}String &operator=(const String &s){if (this != &s){cout << "左值-String& operator=(const String& s)-深拷贝" << endl;delete[] _str; // 防止内存泄漏_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);}return *this;}String &operator=(String &&s) noexcept // 不能使用const,否则无法swap{cout << "右值-- String& operator=(String&& s) -- 移动拷贝赋值效率高)" << endl;swap(_str, s._str);return *this;}~String(){if (_str)delete[] _str;}private:char *_str;
};String f(const char *str)
{return String(str);
}int main()
{String s("123");String s5;s5 = s;s5 = String("临时对象-右值");s5 = f("临时返回值-右值");return 0;
}
运行结果如下:
可以看到,当传入的值是右值时候,会调用移动赋值来减少深拷贝,从而提高效率。
同理:如果注释了移动赋值,传入右值就会调用深拷贝!
3.3 右值引用与返回值问题
正常的传值返回,首先需要深拷贝构造一个临时对象,然后在根据临时对象深拷贝获取我们需要的结果。
而有了右值引用,传值返回的时候就只需要一次深拷贝和一次移动赋值即可。1 深拷贝构造一个临时对象。2 调用移动赋值/拷贝将数据转移给返回的对象。
四. 完美转发 ⭐
完美转发是:在函数模板中,完全依照依照模板的参数类型,将参数传递给函数模板中调用的另一个函数。即保留参数的原始属性和类型传递。
为何需要完美转发:因为在函数传参的过程中,有可能将右值看成左值,这样就不会触发移动语义,造成性能的损失。
4.1 万能引用
当使用 T&&作为模板参数的时候,可以匹配任意类型的左值和右值。
如下面的代码:
#include <iostream>
using namespace std;void Fun(int &x) { cout << "lvalue ref" << endl; }
void Fun(int &&x) { cout << "rvalue ref" << endl; }
void Fun(const int &x) { cout << "const lvalue ref" << endl; }
void Fun(const int &&x) { cout << "const rvalue ref" << endl; }template <typename T>
void PerfectForward(T &&t)
{Fun(t); // 都调用了左值,右值引用会在第二次传参的时候,其属性丢失Fun(move(t)); // 都调用了右值,右值引用会在第二次传参的时候,其属性丢失
}int main()
{int a;PerfectForward(a); // lvalue refPerfectForward(move(a)); // rvalue refconst int b = 8;PerfectForward(b); // const lvalue refPerfectForward(move(b)); // const rvalue refreturn 0;
}
虽然我们传入的值有右值,有左值。但是直接调用Func会导致传入的类型改变。Func直接传入模板的参数,只会调用左值引用的函数。而move参数只会调用右值引用的函数。
运行结果如下:
4.2 std::forward实现完美转发
使用std::forward即可实现完美转发。
通过完美转发,可以帮助完美正确的调用右值,从而实现移动语义,减少拷贝语义带来的性能上的损失。提高代码的效率。
五. 引用总结
1 无论是左值引用还是右值引用,本质都是为了减少不必要的拷贝来提高程序的运行效率。
2 右值引用是左值引用的补充,通过移动构造/赋值减少临时对象的拷贝次数。
左值引用:
1 做参数:解决参数需要拷贝的问题
2 做返回值:用于直接返回在堆上创建的数据,减少拷贝
右值引用:
1 做参数:解决使用临时对象还需要拷贝的问题,直接将临时对象转移到我创建的对象上
2 返回值优化:减少接收返回值的时候需要两次深拷贝的问题,第二次直接将临时对象的资源转移到我接收的对象中。