目录
一.右值引用的作用
回顾左值引用的作用:
- 充当别名,避免拷贝,直接绑定左值,操作同一块内存;
- 允许修改原始对象;
- 函数可以向外带出修改结果(输出型参数);
- 作为返回值,使表达式成为左值,支持连续赋值(如
operator[]);
1.1为什么要引入右值引用?
通过重载区分左右值,针对即将销毁的右值实现移动语义,转移资源而不拷贝数据,消除不必要的内存复制开销。
比如:

在这个例子中传值返回需要创建临时对象,然后再拷贝到外面,这会造成两次构造,导致资源浪费,那么就有同学说了,现在编译器都优化了,构造减少了,资源浪费就少了,还有必要用到右值引用吗?我们先了解为什么编译器要优化
传统只有左值引用传值传参中为了避免造成资源,通常用下面这种方法来写:
形参用引用来接收,改变形参的同时就可以改变外面元素了,但是这里缺少可读性,所以编译器要进行优化来解决该问题

再讲之前我们先来了解一下什么是拷贝构造,拷贝赋值,移动构造和移动赋值

| 函数 | 函数签名 | 时机 | 数据源类型 | 核心行为 |
|---|---|---|---|---|
| 拷贝构造 | string(const string& s) |
创建新对象(初始化阶段) | 左值 /const 左值 | 深拷贝,新建内存复制字符 |
| 移动构造 | string(string&& s) |
创建新对象(初始化阶段) | 右值(临时、std::move) | 资源转移,swap,不拷贝数据 |
| 拷贝赋值 | string& operator=(const string& s) |
对象已存在,赋值覆盖 | 左值 /const 左值 | 释放自身旧资源,深拷贝新数据 |
| 移动赋值 | string& operator=(string&& s) |
对象已存在,赋值覆盖 | 右值(临时、std::move) | swap 转移资源,无数据拷贝 |
总结:拷贝构造和拷贝赋值需要消耗资源,移动构造和移动赋值不需要消耗资源
给出下列代码是为了和库里做区分,可帮助我们看出调用了那些构造
cpp
namespace Jam
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
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);
}
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;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
//cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
//cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
//cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
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];
if (_str)
{
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;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
// 传值返回需要拷贝
string addStrings(string num1, string num2,string& str) {
//string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
}


注:
图一是在Linux上不带优化运行的结果,图二是一代编译器优化的结果,图三是二代编译器优化后的结果
红色框:外部传入函数时需要创建临时对象调用构造然后进行拷贝构造,我们可以看出编译器优化后直接合二为一没有拷贝构造了
绿色框:函数里面临时变量的构造
蓝色框:传值返回时调用的是构造函数创建临时对象,然后再进行拷贝构造,后面可以看出不断被优化,甚至最后只创建了临时对象,里面的局部对象都没有创建
右值引用有移动构造的场景:

通过观察最后一张图,有人就会说,既然编译器都优化了,最后只剩构造,那还要移动构造干什么?
1.不同编译器对优化也不统一,有些编译器优化,有些编译器不优化,所以还是有必要用到移动构造的
2.下面的移动赋值可以体现出右值引用的价值


注:
这里我们可以看出虽然编译器有优化,但是还是会有拷贝赋值这种情况,我们来看一下有移动赋值的情况


虽然这里最后也有移动赋值,但是相比较于拷贝赋值,移动赋值不消耗资源,所以还是有价值的
深拷贝的自定义类型:如 vector/string/map.... 实现移动构造和移动赋值是有很大的价值的
浅拷贝的自定义类型:如 Date/pair<int, int>....不需要实现移动构造和移动赋值。
二.类型分类(了解)


三.引用折叠
出现场景:
只有模板 / 万能引用(T&&)推导的时候才会发生引用折叠! 普通代码手动写 int& && 直接编译报错,不存在折叠。
当类型推导产生引用叠加时,遵循:
只要存在左值引用 &,结果最终就是左值引用 &; 只有两个都是右值引用 &&,结果才是右值引用 &&
T& &→T&T& &&→T&T&& &→T&T&& &&→T&&
口诀:左值引用传染,遇 & 得 &;全 && 才是 &&

万能模板:

cpp
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++; // 部分场景下T带const,打开会报错
}
int main()
{
// 场景1:传入右值 10
Function(10);
// 10是纯右值 → T推导为 int
// T&& = int&& → 实例化:void Function(int&& t)
// 场景2:传入普通左值变量 a
int a;
Function(a);
// a是左值 → T推导为 int&
// T&& = int& && →【引用折叠】→ int&
// 实例化:void Function(int& t)
// 场景3:std::move(a) 亡值(右值)
Function(std::move(a));
// move(a)是右值 → T推导为 int
// T&& = int&& → void Function(int&& t)
// 场景4:const左值 const int b
const int b = 8;
Function(b);
// const左值 → T推导为 const int&
// T&& = const int& && → 折叠 → const int&
// 实例化 void Function(const int& t)
// T = const int&,所以 T x = a; 得到 const int& x;x不可修改,x++编译报错
// 场景5:std::move(b) const右值
Function(std::move(b));
// move(const变量) 是const右值
// T推导为 const int
// T&& = const int&&
// T = const int,T x = a → const int x;依然不能x++
return 0;
}
万能引用推导规律(背诵)
调用 Function(arg)
- arg 是非 const 左值 →
T = 类型&→ 折叠 → 参数类型&(左值引用) - arg 是 const 左值 →
T = const 类型&→ 折叠 → 参数const 类型& - arg 是右值(纯右值 /std::move 亡值) →
T = 类型→ 参数类型&& - arg 是 const 右值 →
T = const 类型→ 参数const 类型&&
传左值,T 推导成引用类型;传右值,T 推导成普通值类型。
四.完美转发
