一、右值引用的定义
1、什么是左值和左值引用?
左值指的是可以出现在等号左边,可以被赋值(非const),可以取地址的值。
左值引用就是左值的引用,给左值取别名。(int& lr = a)
2、什么是右值和右值引用?
右值指的是不能出现在等号左边,不能被赋值,不能取地址的值。
右值引用就是右值的引用,给右值取别名。(int&& rr = 10)
3、区别
①、不能以 可不可以修改,判断左值和右值。
因为左值 const 也不能修改。
②、不能以 在等号左右,判断左值和右值。
因为左值可以出现在等号右边。(但右值不能出现在等号左边)
③、可以用 是否能取地址,判断左值和右值。
因为左值可以取地址,但右值不行。
④、引用的本质是为了减少拷贝!
4、示例
// 返回左值引用
int& func1()
{int* a = new int;return *a;
}// 返回右值,因为有临时拷贝
int func2()
{int a = 10;return a;
}int main()
{int a = 0; // 左值int b = 2; // 左值int& ra = a; // 左值引用/*常见的右值*/// 10// a + b// func2()/*右值引用*/int&& rra = 10;int&& rrs = a + b;return 0;
}
5、总结
1、语法上,引用都是取别名,不开空间,左值引用给左值取别名。右值引用就是给右值取别名。
2、底层,引用都是用指针实现的。左值引用存的是当前左值的地址。右值引用是把右值拷贝到栈上的一个临时空间,再存放这个临时空间的地址。
3、非const 左值引用不能给右值取别名,但const 左值引用可以。
4、 右值引用不能给左值取别名,但是左值 move 后,可以。
二、右值引用的作用
1、左值引用解决的问题
前提环境:VS2019。(VS2022编译器优化太强了,无法观察)
①、左值引用解决了传参效率的问题,减少了拷贝。
// 非引用传参,要进行拷贝
void func(string str)
{}// 引用传参,减少了拷贝
void func(const string& str)
{}
②、左值引用解决了全局对象返回的拷贝问题 (未解决局部对象返回的问题)
// 返回左值引用
int& func1()
{int* a = new int;return *a;
}
2、右值引用解决的问题
①、解决局部对象返回的问题
// Test 类就是包含了一个 char* 变量做的测试
class Test
{
public:Test(const char* str = nullptr){cout << "Test(const char* str = nullptr) -- 构造\n";// 将 str 拷贝进 _strif (str){int size = strlen(str);_str = new char[size + 1];int i = 0;for (i = 0; i < size; ++i){_str[i] = str[i];}_str[i] = '\0';}}void Swap(Test& t){swap(t._str, _str);}Test(const Test& t){cout << "左值 深拷贝 -- Test(const Test& t) -- 拷贝构造\n";// 将 str 拷贝进 _strif (t._str){cout << "深拷贝\n";int size = strlen(t._str);_str = new char[size + 1];int i = 0;for (i = 0; i < size; ++i){_str[i] = t._str[i];}_str[i] = '\0';}}Test& operator=(const Test& t){cout << "左值 深拷贝 -- Test& operator=(const Test& t)\n";// 将 str 拷贝进 _strif (t._str){cout << "深拷贝\n";int size = strlen(t._str);delete _str;_str = new char[size + 1];int i = 0;for (i = 0; i < size; ++i){_str[i] = t._str[i];}_str[i] = '\0';}return *this;}/*Test(Test&& t){cout << "右值 移动拷贝 -- Test(Test&& t) -- 拷贝构造\n";Swap(t);}*/// t1 = t2;/*Test& operator=(Test&& t){cout << "右值 移动赋值 -- Test& operator=(Test&& t)\n";Swap(t);return *this;}*/~Test(){}char* _str = nullptr;
};Test func1()
{const char* str = "abc";Test t(str);return t;
}int main()
{Test t = func1();return 0;
}
我们可以将要释放的右值称作将亡值,反正它都要被释放了,我们就可以通过右值引用(移动拷贝)将它的资源拿出来,就可以通过较低的代价,完成拷贝。
有人可能会有疑问:右值不能修改,为什么移动构造 Test(Test&& t) 中的 t 可以修改?
这是因为 (四.2) 右值被右值引用后,右值引用变量的属性是左值,因此,t 可以被修改.
上述代码,在类 Test 中,有需要深拷贝的成员 char* _str;
如果没有移动构造,就需要先构造,再拷贝构造。
而有了移动构造,就可以先构造,再移动构造。
拷贝构造需要深拷贝,而移动构造只需要交换资源即可!
因为左值不能掠夺资源,而右值可以。
注释掉移动构造:
有移动构造:
拷贝赋值:
Test& operator=(const Test& t){cout << "左值 深拷贝 -- Test& operator=(const Test& t)\n";if (t._str){cout << "深拷贝\n";int size = strlen(t._str);delete _str;_str = new char[size + 1];int i = 0;for (i = 0; i < size; ++i){_str[i] = t._str[i];}_str[i] = '\0';}return *this;}
Test func1()
{const char* str = "abc";Test t(str);return t;
}int main()
{Test t;t = func1();return 0;
}
在没有移动拷贝和移动赋值的情况下:
要进行两次深拷贝。因为分两行写,编译器无法识别,优化。
移动赋值:
Test& operator=(Test&& t)
{cout << "右值 移动赋值 -- Test& operator=(Test&& t)\n";Swap(t);return *this;
}
在有移动拷贝和移动赋值的情况下:
不用进行深拷贝。效率大大提高!
三、右值概念的细分
1、纯右值(内置类型的右值)
如:a + b、10
2、将亡值(自定义类型的右值)
如:匿名对象、传值返回函数 (如上例)
C++ 提供右值引用,就是为了能够区分左值和右值,左值的资源不能乱动,但将亡值我们可以将它的资源进行利用。
3、总结
浅拷贝的类不需要移动构造(右值引用)
只有要深拷贝的类才需要移动构造(右值引用)。
四、有关右值引用的相关知识
1、move 函数
move(leftval)函数的作用是将左值参数变为右值返回,move 以后左值还是左值,只是 move 的返回值是右值。注意:如果 move 后的左值的资源被掠夺,那么该左值的资源也会消失。
int main()
{// Test 类就是包含了一个 char* 变量做的测试// 有关 Test 类的具体定义在上面哦Test t1("aaaa");Test t2(move(t1));return 0;
}
2、右值被右值引用以后,右值引用变量的属性是左值。
Test func1()
{const char* str = "abc";Test t(str);return t;
}int main()
{Test&& r = func1(); // r 是左值Test t(r);return 0;
}
我们可以看到,上述代码中,如果 r 是右值,那么 main() 函数内的 t 应该调移动构造,但它实际上调的是拷贝构造,因此得出结论,r 是左值。
3、万能引用&完美转发
在模版中,为了简化工作,让我们可以不写两份函数区分左值和右值,参数部分直接写为 T&&,模版会自动推演这是左值还是右值 (左值、右值、const 左值、const 右值 都可以匹配),这被称为万能引用。
它也被称作引用折叠,左值就推演为 (T&& -> T&),右值就不变 (T&& -> T&&)。
template<class T>
void PerfectForward(T&& t)
{// 传左值,t就是左值引用// 传右值,t就是右值引用
}
我们知道,t 是左值,如果在 PerfectForward 函数内部还想区分左值和右值怎么办呢?
那就需要完美转发。
完美转发:forward<T>(t) 在传参过程中保持了 t 的原生类型属性。
void func(int& x)
{cout << "func(int& x)" << endl;
}void func(const int& x)
{cout << "func(const int& x)" << endl;
}void func(int&& x)
{cout << "func(int&& x)" << endl;
}void func(const int&& x)
{cout << "func(const int&& x)" << endl;
}template<class T>
void PerfectForward(T&& t)
{// 传左值,t就是左值引用// 传右值,t就是右值引用// func(t); --> 未使用完美转发func(forward<T>(t)); // ---> 完美转发
}int main()
{PerfectForward(10); // 右值int a = 10;int& ra = a;PerfectForward(ra); // 左值引用const int& cra = a;PerfectForward(cra);// const 左值引用return 0;
}
未使用完美转发,全是左值引用。
使用完美转发:保持了原生类型属性
感谢观看♪(・ω・)ノ