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); //error
int&& 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));
}