目录
什么是左值右值?
什么是左值?
左值是一个有具体地址的值。
- 左值可以被取地址。
- 左值既可以在赋值符号左边也可以在右边。
int main()
{
//以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
int d = c;//左值左右都在
return 0;
}
什么是右值?
- 右值不可以取地址,且不能被更改
- 右值只能出现在右边,只能是赋值的那个
int main()
{
double x = 1.1, y = 2.2;
//以下几个都是常见的右值
10;
x + y;
fmin(x, y);
//错误示例(右值不能出现在赋值符号的左边)
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
return 0;
}
没被赋值的数字,然后没有变量接收的算式。
- 右值可以看做一个临时常量或者一个常量值,10是一个常量,x + y fmin(x,y)二者都是一个临时变量,这些都是右值。
- 可以看出这些值并没有被存储,所以右值不能被取地址,只有被接收后才能有地址但这时就变成左值了。
- 右值不允许被取地址和更改
左值引用和右值引用
引用的本义就是给一个值起一个别名。
左值引用
就是给左值起别名,也是用&来声明
int main()
{
//以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
//以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
右值引用
就是给右值起别名,但是通过 &&两个来声明
int main()
{
double x = 1.1, y = 2.2;
//以下几个都是常见的右值
10;
x + y;
fmin(x, y);
//以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double rr3 = fmin(x, y);
return 0;
}
通常来说右值是没有地址的但是在你给它起别名的那时候,他就被赋予了地址,这时其实和左值是类似的,只是在语义上他是右值。那就引申出来一个问题,
左值引用可以引用右值吗?
有两种情况:
1.没有const
就是说:
int& rr1 = 10;
这样是不可以的,因为左值引用是可以修改对应内容的,而右值是不能被修改的,这是一种权限放大所以不行。
2.有const
const int& rr1 = 10;
这时就可以了,因为 此时的右值不能更改,但是有rr1这个地址了,和右值引用类似。
右值引用可以引用左值吗?
不可以,右值引用不可以引用左值,左值是可修改的,虽然这是权限缩小,但是语义上是不行的
但是有一个方法可以就是使用一个C++11配合右值引用的move(左值)函数,此时move后的左值就可以赋值给右值引用。
int main()
{
int a = 10;
//int&& r1 = a; //右值引用不能引用左值
int&& r2 = move(a); //右值引用可以引用move以后的左值
return 0;
}
右值引用使用场景和意义
所以左值引用既可以引用右值又可以引用左值,那还要右值引用干嘛?
现在假设我们在一个string类中的构造函数都加上了一句打印信息
namespace aron
{
//拷贝构造函数(现代写法)
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
swap(tmp); //交换这两个对象
}
//赋值运算符重载(现代写法)
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
string tmp(s); //用s拷贝构造出对象tmp
swap(tmp); //交换这两个对象
return *this; //返回左值(支持连续赋值)
}
}
使用场景:
- 左值引用做参数,防止传参时进行拷贝操作。
- 左值引用做返回值同理
void func1(cl::string s)
{}
void func2(const cl::string& s)
{}
int main()
{
//调用是自己实现的string类里面加了打印信息
aron::string s("hello world");
func1(s); //值传参
func2(s); //左值引用传参
s += 'X'; //左值引用返回
return 0;
}
1.值传参时多调用了一次拷贝构造。
2.+=时如果用的传值返回也会调用一次拷贝构造。
因为string类的拷贝要求的是深拷贝(重新建立一块空间拷贝对象,浅拷贝:和原对象使用一块空间)。
这里看出左值引用已经可以了,但是有些深拷贝是不能避免的。
左值引用的短板
左值引用无法返回局部变量,因为局部变量出了作用域就销毁了,只能使用传值返回。
右值引用就是为了解决左值引用这个短板的
右值引用和移动语义:
解决方法就是给string类增加移动构造和移动赋值
移动构造:
顾名思义也是一个构造函数,这个构造函数的参数是右值引用的,它的做法就是把传入的右值的资源窃取过来,避免深拷贝。小偷构造用了swap函数
我们可以写一个移动构造函数:
namespace aron
{
class string
{
public:
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
和拷贝构造的区别:
- 在没有移动构造前,都是用的拷贝构造左值引用做参数,所以不管传入右值还是左值,都会拷贝一次。
- 增加移动构造后,移动构造的参数是右值引用,其实他就是拷贝构造的一个重载函数,所以如果传入右值时会优先调用移动构造(最匹配原则)。
- 移动构造中调用swap对资源进行窃取转移,因此移动构造的代价比调用拷贝构造的代价小。
在我们需要返回一个局部变量时,本来如果是值返回,但是有移动构造后,直接把局部变量的内容swap过来,此时直接返回,就不用在和值返回一样拷贝构造一个新的返回。
int main()
{
aron::string s = aron::to_string(1234);
return 0;
}
to_string中局部对象是一个左值,这个局部对象(将亡值)出不了作用域,这时如果传值返回的话就会在传出前在进行一次拷贝构造,而如果此时我们直接把它资源拿出来给s 不是更香。
所以“将亡值”我们可以用移动构造传出即可。减少了一次深拷贝
为什么移动构造比拷贝构造代价小?
移动构造是右值时使用,直接把右值的资源换给新的对象即可,而拷贝构造就是重新造一个模子,即重新造一个对象 ,然后这个把这个对象的值换给新对象。
编译器对于这方面的优化
上面不是说,值返回时一般是调用构造函数拷贝出来一个对象,然后把这个对象在调用一次深拷贝给到新对象。
所以说这里应该进行了两次深拷贝,但其实很多编译器在这一步都会进行优化的一步到位。调用一次拷贝构造直接给到新对象
但这一步依然是深拷贝,但还是减少了一次。
而C++11出来以后这个优化依然成立
本来的两次拷贝构造,更换成了两次移动构造。然后优化成一次移动构造,但其实两次移动构造和一次构造的差别不是很大,因为都只是交换资源并没有进行深拷贝。
如果我们是用一个已经存在的对象来接收这个返回值。那这个2->1的优化就没了。
因为对象已经存在了,我们无法直接用拷贝构造给到这个新对象,只能用赋值构造然后给到这个对象,这个是语法问题。
就算是移动构造也需要在进行一次移动构造,然后在进行移动赋值。
移动赋值
和移动构造,移动赋值就是赋值构造的重载函数其中参数是右值引用。
namespace aron
{
class string
{
public:
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
和原本的赋值构造的区别:
和移动构造一样。都是小偷,直接把右值的内容窃取出来。
有了移动构造和移动赋值后,就算是给一个已经存在的对象赋值都没有深拷贝了,因为用移动构造把"将亡值"的内容偷出来后,在用移动赋值把内容给到存在的对象。两次偷的就不用造模子,就快。
STL中的容器在C++11后基本都引入了移动构造和移动赋值。原因就是为了快。
右值引用引用左值之前说要用move函数
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
//forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
_Ty&&_Arg这里看似是一个右值引用,但是因为有了模版所以这里其实是一个万能引用。即这个引用既能是左值也能是右值。
右值引用的使用场景+1:
插入函数:
int main()
{
list<aron::string> lt;
aron::string s("1111");
lt.push_back(s); //调用string的拷贝构造
lt.push_back("2222"); //调用string的移动构造
lt.push_back(aron::string("3333")); //调用string的移动构造
lt.push_back(std::move(s)); //调用string的移动构造
return 0;
}
上面的没出现右值前通通都是进行拷贝构造,都会进行一次深拷贝,而出现右值后,下面的三个直接调用移动构造把资源转移出去,就没有拷贝构造。
完美转发
万能引用
在使用模版的 &&就不是右值引用了,而是万能引用。左值右值都能接收。
template<class T>
void PerfectForward(T&& t)
{
//...
}
万能引用很显然,右值引用是需要通过重载然后确定是右值然后调用右值引用版本,而万能引用则通过传入的参数进行推导
下面代码是通过万能引用拿到不同版本的重载函数:
void Func(int& x)
{
cout << "左值引用" << endl;
}
void Func(const int& x)
{
cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
cout << "右值引用" << endl;
}
void Func(const int&& x)
{
cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
int a = 10;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 20;
PerfectForward(b); //const 左值
PerfectForward(move(b)); //const 右值
return 0;
}
但是我们实际调用了后发现,全都匹配到了const左值或者左值引用的版本。因为右值被引用后是会产生自己的地址,并且也可修改了,所以就被退化识别成了左值。
解决这个问题需要用到完美转发:
template<class T>
void PerfectForward(T&& t)
{
Func(std::forward<T>(t));
}
通过调用std::forward<T>()就能解决。
标签:右值,int,练级,左值,C++,引用,构造,拷贝 From: https://blog.csdn.net/a1275174052/article/details/143993133