目录
左值引用与右值引用
左值引用
右值引用
右值与左值交叉引用
移动语义
移动构造
移动赋值
完美转发
本期我们将学习C++11中比较重要的一个知识点------右值引用。
左值引用与右值引用
在学习左值引用和右值引用之前,我们得先知道什么是左值,什么是右值。
我们直接给出定义。
左值:能够取地址的就是左值。
右值:右值又分为纯右值和将亡值。
纯右值:变量相加(a+b),常数(1,2,3),函数传值返回时最终创建的中间对象。
注意:传值返回的函数返回值与返回时最终创建的中间对象不是一个概念,中间对象的生命周期比函数返回值要大。
将亡值:1.传值返回的函数的返回值(这个返回值通常都是自定义对象)。
2.move以后的自定义对象。
左值引用
图示如下。
以上所有引用都是左值引用,左值引用说白了就是起别名,在某些自定义类型的传参过程中,通过左值引用还可以减少自定义类型对象的拷贝,可以减少资源的消耗。
右值引用
在C++11中,我们引入了右值引用的概念。
图示如下。
右值引用有意义吗?目前看来确实没有什么意义,但是对于后期的移动语义的语法大有作用。
右值与左值交叉引用
提出两个问题。左值引用是否可以引用右值?右值引用是否可以引用左值?我们一起来探究。
图示如下。
不难发现,左值引用和右值引用都不能直接进行交叉引用。但是const左值引用可以引用右值,move之后的左值可以被右值引用引用。
那就意味着在c++98,中左值引用可以引用右值,那么既然如此,右值引用的意义又在哪里,这就回到了上一标题中类似的问题,右值引用的意义仍然在移动语义的语法中大有用处。
移动语义
移动语义又分为移动构造和移动赋值。
我们以之前模拟时间的string类进行探究,模拟实现代码如下,我们已经实现了移动构造和移动赋值。
namespace yjd
{
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)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
this->swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 转移资源" << endl;
swap(s);
return *this;
}
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
//cout << "~string()" << endl;
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;
}
string operator+(char ch)
{
string tmp(*this);
push_back(ch);
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
string to_string(int value)
{
string str;
while (value)
{
int val = value % 10;
str += ('0' + val);
value /= 10;
}
reverse(str.begin(), str.end());
return str;
}
}
移动构造
图示如下。
移动构造和拷贝构造的区别是什么?
通过下述代码为大家讲解。
int main()
{
yjd::string s1 = yjd::to_string(123);
return 0;
}
同样的一段测试代码,在我们屏蔽到移动构造前后运行结果是不一样的。
没有屏蔽前。
屏蔽之后。
为什么屏蔽前后运行结果是不一样的呢?这里面其实大有来头。
我们知道只要是传值返回的函数的返回值一定是通过返回的对象创建出来的中间临时对象。
1.在屏蔽了移动构造之后,在返回调用to_string函数时,to_string函数是一个传值返回的函数,最终返回了str对象,但是str是一个局部对象,出了函数作用域之后会被销毁,所以调用了拷贝构造函数通过str对象创建了中间对象,最终又通过中间对象拷贝构造了s1对象,相当于拷贝构造了两次,但是编译器做了优化,可以直接认为是str对象拷贝构造了对象s1。所以屏蔽之后调用了一次拷贝构造函数,完成了深拷贝。对于string类而言,深拷贝的代价是很大的。
图示如下。
2.在没有屏蔽移动构造之前。 因为to_string返回的是一个临时对象str,我们上文已经讲过,传值返回的返回的临时对象就是将亡值,将亡值也是右值,所以我们通过调用移动构造通过str创建了临时对象,我们又说函数传值返回最终创建的临时对象是纯右值,所以我们又调用移动构造通过临时对象创建了s1。相当于整个过程调用了两次移动构造,但是编译器做了优化,所以整个过程就调用了一次移动构造,认为是str移动构造了s1。
图示如下。
那么拷贝构造和移动构造的区别是什么?
区别很大,拷贝构造是重新申请一块资源拷贝资源,相当于两份同样的资源,计算机消耗的内存资源很大。移动构造是直接进行资源的交换,交换前后资源不变,也就意味着计算机内存资源的消耗很少。
在拷贝构造函数中,我们使用了现代写法通过拷贝构造函数创建了临时对象,然后让当前对象与临时对象发生了交换。所以拷贝构造后,被拷贝的对象的资源仍然是存在的。
在移动构造函数中,因为拷贝的对象要么是将亡值,要么是中间对象(纯右值),都是即将要销毁的,所以在拷贝构造函数中,我们直接对拷贝的对象和被拷贝的对象进行了资源交换,所以在移动构造后,被拷贝的对象的资源已经不在了,已经被转移到了拷贝的对象中。
移动赋值
移动赋值代码图示如下。
测试代码如下。
int main()
{
yjd::string s2 = "hello yjd";
yjd::string s3 = "hello world";
s3 = s2;
s3 = move(s2);
return 0;
}
左值赋值调用赋值运算符重载函数,右值赋值调用移动赋值。
运行结果如下。
赋值运算符重载和移动赋值有什么区别呢?区别还是很大的。
1.赋值运算符重载,赋值前后,赋值的对象的资源仍然不变。
2.移动赋值,因为赋值的是要即将被销毁的右值。赋值之后,赋值的对象的资源与被赋值的对象的资源发生了交换。
总的来说,移动构造和移动赋值与拷贝构造和赋值运算符重载的最大区别就是,移动构造和移动赋值不用去创建新的资源,只是资源的转移,代价比较小;但是拷贝构造和运算符重载中都需要进行深拷贝,代价比较大。比如在拷贝构造函数中创建中间对像时,调用构造函数进行深拷贝,在赋值运算符重载函数中,我我们先拷贝构造一个中间对象,在拷贝构造函数中我们又调用构造函数深拷贝创建了个中间对象。
完美转发
何为完美转发?直接给出概念。
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数且目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销。
简单来说,就是函数模板在向其他函数传递自身形参时,如果相应形参是左值,它就应该被转发为左值;如果相应形参是右值,它就应该被转发为右值。
以一段代码展开。
template<class T>
void Func(T& data)
{
cout << "左值引用" << endl;
}
template<class T>
void Func(const T& data)
{
cout << "const左值引用" << endl;
}
template<class T>
void Func( T&& data)
{
cout << "右值引用" << endl;
}
template<class T>
void Func(const T&& data)
{
cout << "const右值引用" << endl;
}
template<class T>
void PerfectForward(T&& data) //T&& 为万能模板,既可以接收左值也可以接收右值
{
Func(data);
}
int main()
{
PerfectForward(10);
int a = 10;
PerfectForward(a);
const int b = 10;
PerfectForward(b);
PerfectForward(move(b));
return 0;
}
在上述代码中,PerfectForward为转发的模板函数,Func为实际目标函数。
运行结果如下。
为什么我们传入的所有值都是左值引用呢。
这是因为右值再被右值引用之后,就变成了左值,因为可以取地址,如上图所示。所以当我们不论是把右值还是把const右值传递给万能模板之后,data分别被认定成了左值和const左值。
怎么样保证对象本身的属性呢?其实这个问题也就是怎么样保证一个对象传过来的时候是什么类型,发送的时候就是什么类型?
在C++11中,我们通过forward函数实现。
代码如下。
template<class T>
void Func(T& data)
{
cout << "左值引用" << endl;
}
template<class T>
void Func(const T& data)
{
cout << "const左值引用" << endl;
}
template<class T>
void Func( T&& data)
{
cout << "右值引用" << endl;
}
template<class T>
void Func(const T&& data)
{
cout << "const右值引用" << endl;
}
template<class T>
void PerfectForward(T&& data) //T&& 为万能模板,既可以接收左值也可以接收右值
{
Func(forward<T>(data));
}
int main()
{
PerfectForward(10);
int a = 10;
PerfectForward(a);
const int b = 10;
PerfectForward(b);
PerfectForward(move(b));
return 0;
}
运行结果如下。
通过forward函数我们实现了完美转发,即保留了对象的属性。
以上便是本期的所有内容。
本期内容到此结束^_^