前言:
1 左值和右值的认识
2 左值引用和右值引用的区别
2.1 左值引用
2.2 右值引用
3 移动语义
3.1 移动构造
3.2 移动赋值
本篇文章讲解了关于左值引用和右值引用的区别,以及为什么要有右值引用,并展示了右值引用的实际应用等。
如果大家提前不知道什么时左值,什么是右值的话应该会怎么判断呢?是不是左值就是放在左边的值,右值就是放在右边的值?答案很明显不会是这样的,简单举一个例子:
int x = 1;
int y = x;
x + y = 1;
请问我们的int y = x中的x是左值还是右值? x+y = 1的x+y是左值还是右值?
答案就是x是左值,而x+y是右值,当然实际上是不能写为x+y=1的,我这里只是为了方便演示。那么为什么我就能这么笃定x是左值,而x+y是右值呢?
事实上,对于左值和右值我们有一个很简单的判断方式,那就是右值是不会被实际开辟空间的,也就表示右值是无法被获取到地址的,如下:
通过编译所报的错误也表明了取地址&符号只能获取到左值的地址,右值是不行的。
我先给大家展示一些简单的右值引用操作:
double x = 1, y = 2;
double&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
当然,我相信大家看到了这样的代码肯定有一个想法,这右值引用有啥用?感觉和左值引用没有什么区别啊?就是用了两个&&符号?
大家有这样的疑问并没有任何的问题,因为右值引用本来也就不是这样用的,我只是带大家看一下而已,后面会讲解实际的引用场景。
看到下方的代码:请问下面的代码能够编译成功嘛?
int get_num(int& num)
{
return num;
}
void test2()
{
int x = 1, y = 2;
get_num(x + y);
}
int main()
{
test2();
return 0;
}
编译结果:
左值引用并不能引用右值,但是只要我们在左值引用前面加一个const就能行了。
int get_num(const int& num)
{
return num;
}
请问,为什么加上const之后就能够编译通过了呢?咱们先从语义了解以下,其实对于右值来说就是一个临时的变量,它是有常量特性的,所以只要我们加上const是能够接收的,但是这并不能说通,因为我之前说过右值是没有实际地址的,所以就算加上const也不应该能行的。
这确实是一个很好的问题,但是大家有没有想到那就是右值引用是什么时候出来的产物?C++11,那么中间那么长的一段时间都不支持const 左值引用接收右值嘛?
如果不能接收,那么你又如何解释string类,里面的拷贝构造呢?
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
我可是可以通过这样的方式来传值的哦:
yf::string s1("hello world");
yf::string s2(s1 + '!');
我的s1+'!'可是一个右值哦,在C++11之前是没有右值引用的,如果const string& 无法接收,那么又怎么做呢?没办法,带有const 的左值引用必须支持右值的传参。
看了上面的左值接收右值,那么右值可以接收左值嘛?请看下方代码:
void test3()
{
int s1 = 1;
int&& s2 = s1;
}
看来是不行的呢,为什么呢?其实右值涉及到了一个资源释放的问题,马上为大家讲解。
void test3()
{
int s1 = 1;
/*int&& s2 = s1;*/
int&& s3 = move(s1);
}
我们将左值用move移动之后就可以给右值引用了。
其实右值我们也常常称它为将亡值,什么意思呢?也就是快要被销毁的数据,所以通常如果我们不管他的话,右值是使用完了就会被销毁。
但是通过右值引用接收它,它的资源就被交换过来了,网上常常有人说右值引用是延长了变量的生命周期,其实这样的说法并不准确,因为右值这个对象、变量的确是被销毁了,但是我们通过右值引用的方式将他的资源转移过来了,也就是对象的资源的生命周期得到了延长而不是对象本身。相当于我们死了,但是腰子还能给别人用,差不多就是这样。
关于移动还有一个非常好玩的东西,我先为大家展示一下,原理我之后为大家讲解:
我们创建了两个string变量,一个有值,另一个为空,这没问题,然后我们打算通过赋值的方式将s1的值给s2,请看发生了什么事情:
好家伙,有偷子,s1表示,我让你拷贝一份你就给我偷了?s2也很无语哇,说,你都move成为右值,你不是不要了嘛,我拿你的怎么了?
它们谁有问题?没问题,我的锅,我不该这么写。那我就先给一个结论,当该类型支持移动语义的时候,通过move将变量变为右值之后,就会发生这样的事情。
既然上节中带大家见了移动语义会偷东西,那现在就让博主为大家见一下原理吧,对于stl库里面的东西,我们无法直接看到底层,所以博主简易实现了一个string类:
#pragma once
#include
#include
#include
using namespace std;
namespace yf
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s) noexcept
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s) noexcept
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string temp(*this);
temp += ch;
return temp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
yf::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
yf::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
return str;
}
}
// 移动构造
string(string&& s) noexcept
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
看到我们移动构造的写法了嘛?中间的函数体里面的内容直接对s进行了交换,并且我们传入的参数也是一个右值,也就表示了我们不仅能够拿到右值,还能将它的资源拿走,避免了二次拷贝的情况发生。
正常情况下,我们的右值一般不是通过move左值过来的,而是本身就是一个右值,但是对于我们来说却有一些问题了,我们通过右值传参,其实已经是拿到了我们期望的数据了,也就是数据本身是已经计算完成了的,但是由于右值的属性,我们不能对这部分资源进行更改,只能读取这一部分的内容,然后又拷贝一次,这无疑是会让我们的运行效率变低,所以,如果我们能够直接拿到右值的资源,不再又一次的拷贝,效率肯定会提高的。请看下图资源转移过程:
yf::string s1("hello");
yf::string s2(s1 + '!');
s1+'!'是一个右值,但是在此之前,编译器回去计算s1+'!'的值:
计算完成之后,我们看到temp的资源直接被转换到了s下面,这有问题吗?没问题:
然后我们通过交换可以看到,类的数据与s的数据交换了:
这表明了什么?这表示我们将两次深拷贝变为了一次深拷贝加一次移动构造,这两者消耗的时间是一样的吗?不一样,这差距可太大了。
// 移动赋值
string& operator=(string&& s) noexcept
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
对于移动赋值来说和移动构造差不了太多,博主也不演示了,没啥意思。对了,在STL库里的各种容器在C++11更新之后都支持了移动语义,如果有兴趣可以自行查看:
https://legacy.cplusplus.com/
以上就是博主对于右值的全部理解了,希望能够帮助到大家。