个人主页:Lei宝啊
愿所有美好如期而遇
左值
概念
可以取到地址的值就是左值,并且一般情况下可以修改(const类型左值不可修改)。
左值举例:
//左值
int a = 0;
const int b = 1;
int* p = &a;
右值
概念
不能取到地址的值就是右值,并且右值不能被修改。(字面常量,表达式返回值,函数返回值(非左值引用)),我们之前使用的就是左值引用。
右值举例:
int func()
{
return 1 + 1;
}
//右值
10;
a + b;
func();
左值引用
概念
int a = 0;
const int b = 1;
int* p = &a;
//左值引用
int& c = a;
const int& d = b;
int*& e = p;
右值引用
概念
右值引用就是给右值的引用,给右值取别名。
10;
a + b;
func();
//右值引用
int&& e = 10;
int&& f = a + b;
int&& g = func();
互相引用
左值引用右值需要加const,右值引用左值需要将左值先进行move。
//左值引用右值
const int& f = 10;
//右值引用左值
int&& g = move(a);
应用与解释
简单来说就是为什么需要右值引用,我们先来看例子:
也就是说,临时对象的产生其实是多余的,所以在没有右值引用时,编译器给出的优化方案就是优化掉临时对象,直接使func中vv拷贝构造main中vv,可即便如此,还是有一次无谓的拷贝,就是func中的vv,他仍要销毁,资源还是浪费了。
接下来使用我们自己实现的string,来对有无右值引用做对比。
#include <iostream>
#include <string>
#include <string.h>
#include <cassert>
#include <algorithm>
using namespace std;
namespace own
{
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;
_str = new char[s._capacity+1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 拷贝赋值
// s2 = tmp
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
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;
}
const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
own::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
own::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
这里博主使用g++编译器,并且关闭了部分优化,在Visual Studio 2022中,优化非常大,我们看不出他的过程,无法更好地进行对比,所以这里在Linux下进行演示,并使用-fno-elide-constructors关闭g++的编译优化。
举例一:仅有左值引用
一:
int main()
{
own::string ret = own::to_string(1234);
return 0;
}
这也就是我们上面得出的结果。
二:
int main()
{
own::string ret;
ret = own::to_string(-1234);
return 0;
}
这种编译器是不会优化的,所以一般来说将他们写在一行上。
举例二:加入右值引用
// 移动构造 -- 右值(将亡值)
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
我们这里为什么要加入右值引用呢?首先,右值也叫做将亡值,也就是即将销毁的值。我们希望能够将这个将亡值利用起来,拿走他的资源。
我们可以想象一下,可以拿走左值的资源吗?左值引用左值,左值可能仍要被使用,如果这么被swap拿走资源是不可以的,但是将亡值,也就是对右值这样做却是我们希望看到的。
一:
int main()
{
own::string ret = own::to_string(1234);
return 0;
}
这和我们上面的结果是一致的。
二:
int main()
{
own::string ret;
ret = own::to_string(-1234);
return 0;
}
加入移动赋值拷贝
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动拷贝" << endl;
swap(s);
return *this;
}
三:
int main()
{
own::string ret;
ret = own::to_string(-1234);
return 0;
}
看了这么久也许你有一个疑惑,移动构造和移动赋值,都要交换右值的资源,但是右值不是不能被修改吗?
那么这里我们说:右值引用 引用右值后的属性为左值。
也就是说,一个右值,被右值引用后,那个右值引用的属性将变成左值,于是swap中的s属性是左值,也就可以传过去了。