文章目录
- 1. 什么是左值
- 2. 什么是右值
- 3. 左值引用
- 4. 左值引用使用场景
- 5. 右值引用
- 6. 右值引用使用场景
- 6.1 场景1
- 6.2 场景2
- 7. 完美转发
1. 什么是左值
左值不能根据字面意思来理解,不是说在左边的就是左值,例如:
int main()
{int a = 0;int b = a;return 0;
}
这里的int b = a
,a
在右边,但a
也是左值。
左值可以对它进行取地址和赋值,可以出现在赋值=
符号的左边
int main()
{//左值int a = 0;int b = a;int* ptr = new int(11);const int ca = 100;return 0;
}
2. 什么是右值
右值出现在赋值符号的=
右边,不能出现在赋值符号的左边,不能对其进行取地址,例如:
对于
"hello world"
这样的常量字符串,它本身是右值,但是:const char *pch = "hello world";
这个也能对其进行取地址,本质上编译器做出了隐式类型转换,将字符串字面值转换成指向字符常量的指针,将该字符串的首元素地址给了
pch
3. 左值引用
引用在语法上来说就是取别名,左值引用就是给左值取别名:
int main()
{int a = 10;int& b = a; //左值引用return 0;
}
4. 左值引用使用场景
左值引用大多数用于做参数传参和做返回值,采用左值引用的核心价值就是减少拷贝
左值引用缺陷:
但是左值引用有一个缺陷就是不能返回局部对象
string& func() {string s; cin >> s;//...return s; //退出函数生命周期结束 }
这里就算是返回的引用,但是这个局部对象的生命周期到了,所以只能传值返回,这里就有2次拷贝,生成临时对象拷贝一次,赋值又拷贝一次。
class A { public:A() :s(""){cout << "A()" << endl;}A(const string& str) :s(str){cout << "A(const string &str):s(str)" << endl;}A(const A& copy) :s(copy.s){cout << "A(const A& copy)" << endl;}~A(){cout << "~A()" << endl;} private:string s; };A func() {A a;return a; }int main() {A ta = func();cout << endl;A tb;tb = func();cout << endl;A tc(tb); }// 输出: // A() // A(const A& copy) // ~A() // // A() // A() // A(const A& copy) // ~A() // ~A() // // A(const A& copy) // ~A() // ~A() // ~A()
5. 右值引用
右值引用即给右值取别名,用&&
表示:
int Add(int x, int y)
{return x + y;
}int main()
{int&& a = 10;//int& b = Add(2, 3); //errorint&& b = Add(2, 3);return 0;
}
左值引用也可以引用,但是要加上
const
int main() {int x = 20;int y = 30;const int& a = 10;const int& ret = x + y;return 0; }
右值引用需要加上
move
才可以引用左值int main() {int x = 100;int&& y = move(x);return 0; }
6. 右值引用使用场景
右值引用的核心价值也是减少拷贝,弥补左值引用的缺陷(传值返回)
6.1 场景1
自定义类型深拷贝必须返回的场景:
namespace mystring
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}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(string&& s):_str(nullptr){cout << "string(string&& s) -- 移动拷贝" << endl;swap(s);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}string& operator=(string&& s){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){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}mystring::string func()
{mystring::string str("hello");return str;
}int main()
{mystring::string ret1;ret1 = func();cout << endl;mystring::string ret2 = func();return 0;
}
内置类型的右值叫做纯右值;自定义类型的右值叫做将亡值
因为内置类型的生命周期只在表达式这一行,再往后走就析构了,例如
mystring::string ret2 = func()
,这个func()
的生命周期就在这一行。
右值引用就是资源互换,将亡值的资源拿过来,然后把自己不用的内容给将亡值带走
编译器会进行优化:
- 连续的构造/拷贝构造合并;
- 将
str
强行识别成右值——将亡值(每个编译器都这样!)
如果说我们要拷贝的对象是一棵树,这样减少了拷贝的资源,极大提升了性能
6.2 场景2
容器的插入接口,插入对象是右值,可以利用移动构造转移资源,减少拷贝
int main()
{list<mystring::string> lt;mystring::string s1("hello list1");lt.push_back(s1);cout << endl;mystring::string s2("hello list2");lt.push_back(move(s2)); lt.push_back("hello list3"); //插入对象是右值
}
所有的容器都实现了右值版本:
7. 完美转发
在模板中&&
不代表右值引用,而是万能引用,即既能接收左值又能接收右值
void Func(int& x)
{cout << "左值引用" << endl;
}
void Func(int&& x)
{cout << "右值引用" << endl;
}
void Func(const int& x)
{cout << "const 左值引用" << endl;
}
void Func(const int&& x)
{cout << "const 右值引用" << endl;
}template<class T>
void PerfectForward(T&& t)
{Func(t);
}int main()
{int a = 20;PerfectForward(a); //左值PerfectForward(100); //右值const int cb = 30;PerfectForward(cb); //const左值PerfectForward(move(cb)); //const右值return 0;
}
这里运行发现,结果全部都被识别成了左值:
这里是因为引用的底层,都是指针,例如:
int main()
{int a = 10;int& lr = a;int&& rr = move(a);cout << &lr << endl;cout << &rr << endl;
}
这里的lr
是左值,而rr
的属性也是左值,右值是不能够修改和取地址的,而这里的rr
既能取地址也能修改。
这里有点绕,比如说
hello
,这个字符串是右值,而string&& s = hello
,s
是它的右值引用,s
会开一个空间把这个值存起来,s
的属性还是属于左值的,因为它能够修改和取地址
即右值引用变量的属性会被编译器识别成左值,不然在移动构造的场景下,就无法进行资源转移。
要想其保持原有属性:左值引用左值属性,右值引用右值属性
可以完美转发forward
:
void PerfectForward(T&& t)
{Func(forward<T>(t));
}