简单剖析C++右值引用和左值引用
左值与右值的区别
左值可以有固定的名字,可以在声明所在的作用域内被任意调用;
右值没有固定的名称,匿名变量就是很好的例子,并且由于右值在作用域内没有固定的地址,因此不可以被访问。
右值和左值应用实例
#include <iostream>
using namespace std;
#include <algorithm>
void ShowInf(int&& var) {
cout << "调用右值引用" << endl;
cout << var << endl;
}
void ShowInf(int& var) {
cout << "调用左值引用" << endl;
cout << var << endl;
}
int main() {
int a = 5;
ShowInf(a);
ShowInf(9);
}
我们看到,在参数为&&的函数中,我们传入了临时整型常量5,此时,5的存储单元被赋予一个临时名字,此时这块内存可以在函数showing这被访问;
在参数为&的函数中,我们传入了整形变量a,此时,传入参数是a本身,是a变量的引用。
但是,我们用如下代码可以吗:
#include <iostream>
using namespace std;
#include <algorithm>
void ShowInf(int& var) {
cout << "调用左值引用" << endl;
cout << var << endl;
}
int main() {
int a = 5;
ShowInf(9);
}
将右值传入参数为左值引用的函数中,或者:
#include <iostream>
using namespace std;
#include <algorithm>
void ShowInf(int&& var) {
cout << "调用右值引用" << endl;
cout << var << endl;
}
int main() {
int a = 5;
ShowInf(a);
}
将左值传入参数为右值引用的函数中。
这是绝对不可以的,对于左值来说,传入的参数得是有确定存储地址的变量,即有确定地址的量,当你传入一个无法被访问的临时变量时,该函数无法正常运行;同样地,对于右值来说,传入参数得是没有确定地址的变量,当你传入一个可以被访问的变量时,该函数无法正常运行!
左值右值互相转换
右值->左值
#include <iostream>
using namespace std;
int main()
{
const int& var = 6; // 右值赋给左值引用
}
const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const &作为函数参数的原因之一。
Const引用将右值转化为左值的原理:
将右值赋值给临时变量,临时变量也是变量,只不过在一般情况下,临时变量用完之后随机释放掉,但是此时并不是这样,左值引用的特殊之处就在于将这个临时变量转化为一个“正式的变量”,即这个变量用完后不再被释放,而是作为一个变量存在于自己的作用域中。
左值->右值
std::move是一个非常有迷惑性的函数,不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)。
#include <iostream>
using namespace std;
#include <algorithm>
int main()
{
const int& var = 6; // 右值赋给左值引用
const int&& var1 = move(var); // 务必要确定,左值引用和右值引用相互转换时数据类型一定要相同
int var2 = 10;
int&& var3 = move(var2); // 将左值强转为右值并将值赋值给var3右值引用
}
右值引用和左值引用的本质
右值引用:将临时变量变为在自己作用域内一直存在的真正变量;
左值引用:给变量取个别名;
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int var = 10;
int& var1 = var; // 左值赋给左值引用
int&& var2 = move(var1); // 左值赋给右值引用
int&& var3 = static_cast<int&&>(var2); // 左值赋给右值引用
}
我们看到:最后一个将var2左值变量强转为右值后赋值给右值引用,我们不禁疑问,var2是左值吗?var2不是右值引用吗?难道右值引用也是左值吗?
右值引用和左值引用本质上都是左值,左值就是我们在平常可以用变量名访问操作的变量,显然右值引用和左值引用都符合这个条件。
① 左值引用的作用:
给已存在的左值起个别名方便操作;
② 右值引用的作用:
给没有名字的变量分配各固定的地址,省去了“值->临时变量->左值变量”中“临时变量->左值变量”这个过程,即“值->左值变量”进执行一次拷贝即可,效率大大提高。
Move的升级版-forward函数实现移动语义
#include <iostream>
using namespace std;
#include <algorithm>
int main()
{
int var1 = 10;
int&& var2 = forward<int&&>(var1); // 左值->右值
int var3 = forward<int>(10); // 右值->左值
}
Forward的优势就在于这个函数可以实现“左值->右值”和“右值->左值”,而move函数只能实现“左值->右值”。
移动语义有何用处?
① 提高程序运行效率
例如标准库的可伸缩数组 vector 的复制操作的时间复杂度是O(n) 移动操作的时间复杂度是 O(1)。在明知道移动可行的情况下利用移动语义可以生成更快的代码
② 避免深拷贝
使用移动语义避免深拷贝的方式很特别,原理如下:
代码示例:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
class Student
{
public:
int age;
char* name;
public:
Student(char* name, int age)
{
this->name = new char[strlen(name) + 1];
memset(this->name, 0, strlen(name) + 1);
strcpy(this->name, name);
this->age = age;
}
Student(Student& obj)
{
this->name = new char[strlen(obj.name) + 1];
memset(this->name, 0, strlen(obj.name) + 1);
strcpy(this->name, obj.name);
this->age = obj.age;
}
Student(Student&& obj)
{
this->name = obj.name;
obj.name = nullptr;
this->age = obj.age;
}
void ShowInf()
{
if (this->name != nullptr) {
cout << this->name << "的年龄为" << this->age << endl;
}
else {
cout << "内容为空" << endl;
}
}
~Student()
{
if (this->name != nullptr)
{
delete[] this->name;
}
}
};
int main()
{
char* name = new char[4]{ "666" };
Student obj(name, 17), obj1(obj), obj2(move(obj1));
obj.ShowInf();
obj1.ShowInf();
obj2.ShowInf();
}
在上述代码中,我们看到当我们使用移动语义将obj1的内容拷贝到obj2中时,我们置空了obj1中的指针,将obj1的指针直接赋值给了obj2,从此以后obj1这个对象我们就不再用了,我们从此使用obj2代替obj1。
因此,可移动对象在“需要拷贝且被拷贝者之后不再被需要”的场景,建议使用std::move触发移动语义,提升性能。