C++中的 左值和右值
简单地说:
左值 就是赋值符号 = 左边的值,是一个可以被改变的值
但赋值符号 = 右边的值不一定是 右值 ,右值是指不可以被改变的值(例如:常量、表达式返回值、临时对象)
移动语义
将一个对象中的资源转移到另一个对象中的方式,称为移动语义。
大家可以先看下面的一段代码,试着理解:
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;
}
// s1 += s2 体现左值引用,传参和传值的位置减少拷贝,提高效率
String& operator+=(const String& s)
{
//this->Append(s.c_str());
return *this;
}
// s1 + s2
String operator+(const String& s)
{
String tmp(*this);
//tmp.Append(s.c_str());
return tmp;
}
const char* c_str()
{
return _str;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("hello");
String ret;
ret = s1 + s2;
return0;
}
下面我将通过图解分析帮助大家理解:
运行上面的代码,我们发现了什么问题呢?
其实上面的代码运行结果不会有错误,但是效率相对较低。
首先拷贝构造的TMP对象刚创建只要被(s1+s2)拷贝构造,就会立马被销毁(空间释放),再没有其他作用;但是(s1+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;
}
通过上面的代码,我们发现移动语句的完成,主要是通过swap()函数完成了资源的交换,从而提高了代码的效率。
通过运行,我们发现完成ret的计算这时只需要一次拷贝构造(tmp拷贝构造TMP)
,一次移动构造(TMP与(s1+s2)交换资源)
,一次移动赋值((s1+s2)与ret交换资源)
。
注意: 在C++11中,为了保证类同时具有拷贝和移动语义,则拷贝构造,移动构造,赋值,移动赋值必须同时提供或者同时不提供。
那么移动语义在什么时候会被主动调用呢?
在C++11中如果需要实现移动语义则必须使用右值引用。
右值引用
右值引用顾名思义,就是对右值的引用。
C++11 中,右值主要由两个概念组成:纯右值和将亡值
纯右值:用于识别一些临时变量和不和对象关联的值,比如:常量,运算表达式等
将亡值:声明周期将要结束的对象。比如:在值返回时的临时变量
书写格式:
类型 && 引用变量名字=实体;
应用场景:
1)与移动语义结合使用,可实现减少无必要资源的开辟,从而提高代码运行效率。
2) 給匿名对象取别名,延长匿名对象的生命周期。
使用说明:
1)与引用一样,右值引用在定义时必须初始化
2)通常情况下,右值引用不能引用左值
int main()
{
int a = 10;
//int && ra; //编译失败,引用没有进行初始化
int & la = a; //左值引用左值
int && ra = 10; //右值引用右值
//int &lb = 10; //常规条件下,左值引用右值无法通过编译
const int &lb = 10; //但用const 保证引用左值不被改变的话,可以引用右值
//int &&rb = a; //常规条件下,右值引用左值无法通过编译
int &&rb = move(a); //move语句可以将左值变成右值,从而完成引用
return 0;
}
std::move()
C++11 中,std::move()函数并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。
注意:这里被转化为右值的左值,它的生命周期不会变化。
int main()
{
vector<string> v;
string str("world");
v.push_back(str); //str 作为左值会通过拷贝完成
//v.push_back(move(str)); //str作为右值会直接交换资源
v.push_back("hello");
return 0;
}
完美转发 forward ()
1)可以保持数据原有的属性,即左值通过转发后仍然是左值,右值通过转发后仍然是右值。
2)完美转发是目标函数总是希望将参数按照传递给转发函数的实际类型转发给目标函数,而不产生额外的开销,就好像转发者不存在一样。
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;
}
右值引用的讲解暂时就到这里啦,之后我会继续更新C++11的其他内容,欢迎大家留言讨论~
【C++11】lambda表达式