个人主页: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 = tmpstring& 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属性是左值,也就可以传过去了。