1.什么是左值和右值
C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量。左值表示存储在计算机内存的对象,而不是常量或计算的结果。或者说左值是代表一个内存地址值,并且通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这也就是为什么左值可以被赋值的原因了。相对应的还有右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的“右值”,也就是其代表的真实值。 简单的来说其实赋值符号左边的叫左值(是可以被修改的值) ,赋值符号右边的不一定 为右值(若是不可以改变的就称为右值 ,如:常量,表达式返回值,临时变量等) 下面举个例子看看什么是左值和右值
int a = 3;
const int b = 5;
a = b+2; //a是左值,b+2是右值
b = a+2; //错!b是只读的左值但无写入权,不能出现在赋值符号左边
(a = 4) += 28; //a=4是左值表达式,28是右值,+=为赋值操作符
34 = a+2; //错!34是字面量不能做左值
2.左值引用和右值引用
左值引用左值 int& lr1 = a; lr1 = 20;
右值引用右值 int&& rr1 = 10;rr1 = 20;
左值引用右值 int& lr2 = 10;不支持 const int& lr2 = 10;特例,可以支持
右值引用左值 int&& rr2 = a; 不支持 int&& rr2 = move(a);特例,可以支持
3.C++11中的右值
纯右值 :纯右值是C++98中的右值概念,用于识别临时变量和一些不跟对象关联的值。比如:常量,一些运算表达式(1+3)等。将亡值 :生命周期将要结束的对象,比如:值返回时创建的临时变量
4.右值引用的应用
我们先来看看STL中的string类,我们知道如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果我们遇到返回一个临时对象时,我们应该怎么处理,才能提高效率,我们来看看。
class String
{
public:
String(char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造
// String s(左值对象)
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
cout << "String(const String& s)" << endl;
strcpy(_str, s._str);
}
// s1 = s2 正常赋值
String& operator=(const String& s)
{
cout << "String& operator=(const String& s)" << endl;
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
~String()
{
if (_str) delete[] _str;
}
const char* c_str()
{
return _str;
}
private:
char* _str;
};
移动语义
假设现在有一个函数,返回值为一个String类型的对象: 上述代码看起来没有什么问题,但是有一个不太尽人意的地方:GetString函数返回的临时对象,将s2拷贝 构造成功之后,立马被销毁了(临时对象的空间被释放),再没有其他作用;而s2在拷贝构造时,又需要分配 空间,一个刚释放一个又申请,有点多此一举 。那能否GetString返回的临时对象的空间直接交给s2呢? 这样s2也不需要重新开辟空间了,代码的效率会明显提高。
将一个对象中资源移动到另一个对象中的方式,称之为移动语义 。在C++11中如果需要实现移动语义,必须使用右值引用 。
移动构造
我们来理解一下移动构造,看代码其实很好理解,就是将一个对象的资源移动给另一个对象,并将该对象置空。 为什么要这么做呢,我们刚刚在上面提到将亡值这个概念,什么是将亡值,就是生命周期马上要结束的值,那按照我们一般的拷贝构造,在返回一个临时对象时,会进行深拷贝,这里的临时对象就是一个将亡值,我们对其进行深拷贝是一种浪费资源和浪费时间的行为,那我们用这个移动构造,直接将将亡值的资源移动到新的对象中,减少了拷贝,提高了效率。
// 移动构造
// String s(将亡值对象)
String(String&& s)
:_str(nullptr)
{
cout << "String(String&& s)" << endl;
swap(_str, s._str);
}
移动赋值
// 移动赋值
// s1 = s2
String& operator=(String&& s)
{
cout << "String& operator=(String&& s)" << endl;
swap(_str, s._str);
return *this;
}
左值引用
重载运算符+=体现了左值引用,我们来看看具体是怎么实现的。
// s1 += s2 体现左值引用,传参和传值的位置减少拷贝,提高效率
String& operator+=(const String& s)
{
//this->Append(s.c_str());
return *this;
}
右值引用
重载运算符+,体现了右值引用,我们也来看一下是怎么实现的。
// s1 + s2 体现右值引用,将亡值tmp自动调用移动构造和移动赋值以减少拷贝
String operator+(const String& s)
{
String tmp(*this);
//tmp.Append(s.c_str());
return tmp;
}
上述例子我们很容易看出,右值引用的出现也是为了减少拷贝,提高效率,而且它的出现并未改变重载运算符的实现,而是在返回一个临时对象,判断其为将亡值从而自动调用移动构造和移动赋值来减少深拷贝,以提高效率 。
5.完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。 简单的来说,就是完美转发可以保持数据的原来属性,原来是左值经过完美转发之后还是左值,原来是右值完美转发还是右值。 举个例子
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(std::forward<T>(t)); }
int main()
{
PerfectForward(10); // rvalue re
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
//const int b = 8;
//PerfectForward(b); // const lvalue ref
//PerfectForward(std::move(b)); // const rvalue ref
return 0;
}