左值引用和右值引用
在C++中,左值是一个表示数据的表达式,我们可以获取它的地址,一般可以对它赋值,通常可以出现在左边或右边,左值引用就是对左值的引用,相当于给左值起了一个别名。
例子:
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
右值同样也是一个表示数据的表达式,如:字面常量、表达式的返回值、函数的返回值。我们不能获取它的地址,不能对它赋值。右值可以出现在赋值符号的右边,但不能出现在左边。右值引用就是对右值的引用,相当于给右值起了一个别名。
例子:
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
左、右值引用的区别
关于左值引用的总结
1. 左值引用可以绑定左值,但不能绑定右值。
2. const左值引用既可以绑定左值又可以绑定右值。
例子:
int a = 10;
int& ra1 = a; // 左值引用
//int& rn = 10; ---编译器报错,因为左值引用不能绑定右值
const int& rn = 10; // const左值引用可以绑定右值
const int& ra2 = a; // const左值引用当然也可以绑定左值
关于右值引用的总结
1. 右值引用可以绑定右值,但不能绑定左值。
2. 右值引用可以绑定左值调用move后的返回结果。
例子:
int&& rn = 10; // 右值引用当然可以绑定右值
int a;
//int&& ra1 = a; ---编译器报错,因为右值引用不能绑定左值
int&& ra2 = std::move(a); // 右值引用可以绑定左值调用move后的返回结果
// 请注意,左值在调用move后,它本身依然是左值,还是不能被右值引用绑定
右值引用的使用场景
在前面的学习中,我们对左值引用和右值引用有了最基本的了解,可能有同学会好奇了,左值引用好像就能绑定左值和右值了,为什么c++11还要引入右值引用这个技术呢?所以接下来让我们看看左值引用的短板,以及右值引用是如何弥补这一短板的吧!
首先,回忆一下我们以前学习过的左值引用的使用场景,因为左值引用相当于给变量起了别名,所以当我们传递参数或者返回函数的结果时,可以用左值引用来减少不必要的复制。但是,单就这两种场景,左值引用就真的能够完全胜任吗?
我们知道,函数中定义的局部变量出了函数作用域就会被释放,那么如果我们使用左值引用返回这个局部变量,就会出现悬空引用的问题,于是在没有右值引用之前,我们不得不直接返回值,所以左值引用不能对这种情况进行优化。
而右值引用正是为了处理这一场景而出现的,C++中,我们把右值分为纯右值和将亡值,如下图这个例子中,变量ret即将被销毁,就属于将亡值。刚刚一门一直在说不必要的复制,那具体是复制什么呢?
我们知道,ret作为一个字符串类型的变量,我们需要为它开辟一块内存空间,然而这个将亡值在出了函数作用域之后就被销毁了,要怎么把返回值传递给s呢?其实这涉及到一个小知识点:当函数返回一个局部变量时,编译器会创建一个临时变量来持有返回值。
所以说,这个例子其实是这样:编译器创建临时变量时调用拷贝构造进行一次深拷贝,开辟了一块内存空间把字符串存了进去,然后ret这个局部变量被销毁时,自己的内存空间也被释放;临时变量赋值给s时调用赋值重载再进行一次深拷贝,有开辟了一块内存空间,把字符串存了进去,然后临时变量的内存空间也被释放。
不难发现,这个过程中,我们进行了两次完全没有必要的深拷贝,而且释放这些空间也是有一定消耗的。机智如你肯定已经发现了端倪,既然这个ret和临时变量本来就快被销毁了,那为啥还要专门给它们开辟内存呢?直接这样,把ret的内存空间转移给临时变量,再把临时变量的内存空间转移给s不就完了吗?是的!其实两步就是传说中的移动构造和移动赋值重载。
那这和右值又有什么关系呢?这是因为转移内存空间这种事还是有点危险的,如果所有类型的变量都能这样做不就乱套了吗!所以移动语义只能通过右值引用来完成,因为应用场景本来就也只是对将亡值进行资源转移嘛。
因为移动语义能够弥补左值引用不能返回局部变量的短板,大量减少不必要的深拷贝和释放空间,所以stl容器基本都引入了移动构造和移动赋值重载:
移动语义的简单实现
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
#include <cassert>
#include <algorithm>
using namespace std;
namespace MySTL
{
// 我们为了方便测试移动语义自己写的string类
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)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
/*string tmp(s);
swap(tmp);*/
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
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)
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
};
MySTL::string to_string(int x)
{
MySTL::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;
}
}
int main()
{
MySTL::string s;
s = MySTL::to_string(1234);
return 0;
}
完美转发
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
请看下面的代码:
void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
template<class T>
void PerfectForward(T&& t)//万能引用
{
Fun(t);
}
int main()
{
PerfectForward(10);//右值
int a;
PerfectForward(a);//左值
PerfectForward(std::move(a));//右值
const int b = 8;
PerfectForward(b);//左值
PerfectForward(std::move(b));//右值
return 0;
}
第一层per函数的参数既能接受左值,也能接受右值,但是假如你把代码复制后测试,会发现在参数传递到第二层函数时,它全部变成的左值,这是因为模板中的万能引用会将右值退化成左值,所以后续
使用过程它就变成了左值!
使用forward可以保留对象的原生类型
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}