文章目录
C++11 引入了右值引用,这是C++语言的一个重要特性,目的是为了提高程序的性能,尤其在对象的传递和资源管理方面。
右值引用和左值引用相比,解决了左值引用在传返回值的不足,显著减少了不必要的拷贝,提高效率。
右值和左值的基本概念
在 C++ 中,表达式的值可以分为左值和右值两种类型:
左值
:表示一个持久存在的对象或者内存位置,通常在赋值语句的左侧出现,以及有可以取地址的特性。例如:变量、数组元素、解引用等都是左值。
//以下均是左值
//变量
int a = 3;
int* pa = &a;
const int b = a;
int* ptr = new int(3);
//解引用
*ptr = 4;
//数组元素
string str("abcdef");
str[0];
右值
:表示临时对象、字面量常量或者表达式的结果,通常只能出现在赋值语句的右侧,有不可取地址的特性。右值是没有名称的、即将被销毁的对象。
int a = 4, b = 5;
//以下均是右值
100;
a + b;
fmin(x, y);
string("qwer");
左值引用和右值引用
引用就是给对象取别名,右值引用就是给右值取别名,左值引用就是给右值取别名。右值引用
和左值引用
在语法形式上是类似的:
Type& ref = x; //左值引用
Type&& rref = y; //右值引用
可以看到,左值引用是用 &
,而右值引用是用 &&
。
//左值
int a = 3;
int* pa = &a;
const int b = a;
int* ptr = new int(3);
//左值引用
int& ra = a;
int*& rpa = pa;
const int& rb = b;
int* rptr = ptr;
int a = 4, b = 5;
//右值
//100;
//a + b;
//fmin(x, y);
//string("qwer");
//右值引用
int&& rr1 = 100;
int&& rr2 = a + b;
int&& rr3 = fmin(a, b);
string&& rr4 = string("qwer");
对右值引用的理解:右值本质上是一种生命周期很短的对象(将亡值),而右值引用实际上是将该对象的地址保存,该对象就不会立即销毁,延长了生命周期。
注意,通过
右值引用创建出来的对象的属性是左值
,这一点非常的重要,涉及到下面提及的完美转发。
一般而言,右值引用只能引用右值,左值引用只能引用左值,但在特殊情况下,右值引用可以引用左值,左值应用也可以引用右值 。
- 左值引用去引用右值:需要在前面加
const
修饰。 - 右值引用去引用左值:需要对左值进行
move
。
//左值引用去引用右值,需要加const
const int& r1 = 10;
const string& r2 = string("abcd");
//右值引用求引用左值,需要对左值move
int x = 3;
int&& rr1 = move(x);
string str("1234");
string&& rr2 = move(str);
左值引用在特定条件下可以引用右值,这一点在前面其实也有所涉及,之前模拟实现容器(如 vector、list等)的 push_back
函数: void push_back (const T& x)
,加 const
是为了让 x
既能接收左值也能接收右值。
move
本质上就是强制类型转换,不会改变左值对象本身的属性。
template <class T>
typename remove_reference<T>::type&& move (T&& arg) noexcept;
{
return static_cast<remove_reference<decltype(arg)>::type&&>(arg)
}
右值引用的主要用途
在右值引用出现之前,左值引用还是无法解决在某些场景下需要传值返回的问题,而右值引用的出现,实现了移动语义
和完美转发
,显著提高C++程序在对象的的拷贝和传递的性能。
移动语义
移动语义可以分为移动构造
和移动赋值
,其实就可以“移动”资源而不是复制资源。从而避免不必要的资源拷贝。右值引用允许了资源从一个对象转移到另一个对象,而不是创建一个新的副本。
接下来,我们就以一个自定义 string
类,来看看移动语义的作用是多么强大,还有在没有移动语义之前VS的设计者如何跟冗余构造斗智斗勇。
class string
{
public:
//构造
string(const char* str = "")
{
cout << "string(char* str) -- 构造" << endl;
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//析构
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
string tmp(s._str);
swap(tmp);
}
//赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 赋值重载" << endl;
if (this != &s)
{
_str[0] = '\0';
if (s._capacity > _capacity)
{
char* tmp = new char[s._capacity + 1];
if (_str)
delete[] _str;
_str = tmp;
_capacity = s._capacity;
}
strcpy(_str, s._str);
_str[s._capacity] = '\0';
_size = s._size;
}
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
只有拷贝构造和赋值重载而没有移动语义的传值返回
正常对一个自定义类传值返回是需要进行3次构造的,函数体内将构造需要返回 str
对象,在返回 str
时先对其拷贝构造出一个临时对象 tmp
,函数体外的用于接收返回值的 ret
再去拷贝构造这个 tmp
对象,很明显,这样多次构造消耗很大,效率很低。如下图:
聪明的编译器设计师一想,这样不慢了啊,干脆不构建临时对象,直接将 str
拷贝构造给 ret
不就行了。如下图:
另一位设计师看了,不行啊,你这样还是慢,看我的,直接将3次构造合三为一。如下图:
我们可以运行一下来验证结果,如下图:
结果就是编译器真的做出了 “合3为1”
的极致优化来提高效率,这里真的不得不感叹下设计编译器的设计师能力是真的强