欢迎来到我的频道 【点击跳转专栏】
码云链接 【点此转跳】
文章目录
- [1. C++11的发展历史](#1. C++11的发展历史)
- [2. 列表初始化](#2. 列表初始化)
- [2.1 C++98传统的{}](#2.1 C++98传统的{})
- [2.2 C++11中的{}](#2.2 C++11中的{})
- [2.3 C++11中的std::initializer_list](#2.3 C++11中的std::initializer_list)
- [3. 右值引⽤和移动语义](#3. 右值引⽤和移动语义)
- [3.1 左值和右值](#3.1 左值和右值)
- [3.2 左值引⽤和右值引⽤](#3.2 左值引⽤和右值引⽤)
- [3.3 引⽤延⻓⽣命周期](#3.3 引⽤延⻓⽣命周期)
- [3.4 左值和右值的参数匹配](#3.4 左值和右值的参数匹配)
- [3.5 右值引⽤和移动语义的使⽤场景(重难点)](#3.5 右值引⽤和移动语义的使⽤场景(重难点))
- [3.5.1 左值引⽤主要使⽤场景回顾](#3.5.1 左值引⽤主要使⽤场景回顾)
- [3.5.2 移动构造和移动赋值](#3.5.2 移动构造和移动赋值)
- [3.5.3 右值引⽤和移动语义解决传值返回问题](#3.5.3 右值引⽤和移动语义解决传值返回问题)
- [1. 右值对象构造,只有拷⻉构造,没有移动构造的场景(当你不写移动构造的时候)](#1. 右值对象构造,只有拷⻉构造,没有移动构造的场景(当你不写移动构造的时候))
- 2.右值对象构造,有拷⻉构造,也有移动构造的场景(优先走移动构造)
- [3. 右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景](#3. 右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景)
- 4.右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
- [3.5.4 右值引⽤和移动语义在传参中的提效](#3.5.4 右值引⽤和移动语义在传参中的提效)
- [3.6 类型分类](#3.6 类型分类)
- [3.7 引⽤折叠&&万能引用的引出(重难点)](#3.7 引⽤折叠&&万能引用的引出(重难点))
- [3.8 完美转发&&万能引用的实践(重难点)](#3.8 完美转发&&万能引用的实践(重难点))
- [3.8.1 利用万能引用改造list](#3.8.1 利用万能引用改造list)
- [3.8.2 深度剖析3.8.1 如何做到完美转发?](#3.8.2 深度剖析3.8.1 如何做到完美转发?)
1. C++11的发展历史
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既有实践,并改进了对 C++ 程序员可⽤的抽象。在它最终由 ISO 在 2011 年 8 ⽉ 12 ⽇采纳前,⼈们曾使⽤名称"C++0x",因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故⽽这是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次。

2. 列表初始化
2.1 C++98传统的{}
C++98 中一般数组和结构体可以用 {} 进行初始化。
cpp
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
2.2 C++11中的{}
- C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
- 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
- {}初始化的过程中,可以省略掉=
- C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器
push/inset多参数构造的对象时,{}初始化会很方便
我们的老朋友 Date类 篇幅问题 图片带过
cpp
// C++11
int x1 = 1;
int x2 = { 1 };
int x3{ 1 };
// ⾃定义类型⽀持
// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化
Date d1(2025, 1, 1);
Date d2 = { 2025, 9, 18 };
Date d3{ 2025, 9, 18 };
// 这⾥d4引⽤的是{ 2024, 7, 25 }构造的临时对象 临时对象有常性
const Date& d4 = { 2024, 7, 25 };
// 需要注意的是C++98支持单参数时类型转换,也可以不用{}
Date d5 = { 2025 };
Date d6 = 2025;
vector<Date> v;
v.push_back(d1);
v.push_back(Date(2025, 1, 1));
// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
v.push_back({ 2025, 1, 1 });
Date d7 = { 2025, 9, 18 }; // 调用3个参数构造
2.3 C++11中的std::initializer_list
- 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持,如:
vector<int> v1 = {1,2,3};vector<int> v2 = {1,2,3,4,5}; - C++11库中提出了一个
std::initializer_list的类,auto il = { 10, 20, 30 }; // the type of il is an initializer_list,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。 std::initializer_list支持迭代器遍历。- 容器支持一个
std::initializer_list的构造函数,也就支持任意多个值构成的{x1,x2,x3...}进行初始化。STL中的容器支持任意多个值构成的{x1,x2,x3...}进行初始化,就是通过std::initializer_list的构造函数支持的。
cpp
vector<int> v1 = { 1,2,3,4,5,6,7,8,8}; // 任意多个int值的{}列表
v1 = { 1,2,3 };
auto il = { 1,2,3,4,5,6,7,8,8 };
//当使用 auto il = { ... };(花括号初始化列表)时,il 的类型是 std::initializer_list<int>
cout << sizeof(il) << endl;//每个指针占 8 字节 总共 8 + 8 = 16 字节
//std::initializer_list<T> 在标准库中的典型实现是 两个指针(或一个指针 + 一个 size)
cout << typeid(il).name() << endl;//这会输出 il 的类型名称,但具体字符串是编译器相关的(
// 这里是pair对象的{}初始化和map的initializer_list构造结合到一起用了
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
3. 右值引⽤和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
3.1 左值和右值
- 左值 是一个表示数据的表达式(如变量名或解引用的指针 ),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值 也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等 ,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 值得一提的是,左值 的英文简写为
lvalue,右值 的英文简写为rvalue。传统认为它们分别是left value、right value的缩写。现代C++中,lvalue被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而rvalue被解释为read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
| 特性/场景 | 左值(lvalue) | 右值(rvalue) |
|---|---|---|
| 核心定义 | 可寻址、有持久存储的表达式 | 不可寻址、临时或字面量的表达式 |
| 能否取地址 | ✅ 可以(如 &a) |
❌ 不可以(如 &10 非法) |
| 能否在赋值号左侧 | ✅ 可以(如 a = 10) |
❌ 不可以(如 10 = a 非法) |
| 典型示例 | ||
| 变量/对象 | int a = 10;(a 是左值) |
字面量:10、3.14、"hello" |
| 指针解引用 | int* p = &a; *p = 20;(*p 是左值) |
表达式临时结果:a + b、a * 5 |
| 数组/容器元素 | arr[0] = 1;(arr[0] 是左值) |
函数返回值(非引用):int foo(); |
| 函数返回值 | 返回左值引用:int& foo(); foo() = 10; |
匿名对象:string("abc") |
| const 修饰 | const int x = 10;(x 是左值,不可赋值但可寻址) |
类型转换结果:(double)a |
| 迭代器 | *it = 5;(*it 是左值) |
lambda 返回值:auto f = []{return 10;}; f(); |
案例:
cpp
#include <iostream>
#include <string>
#include <algorithm> // 包含fmin函数的头文件
using namespace std;
int main()
{
// 左值核心特征:可以取地址、有持久存储、可出现在赋值号左侧(const左值除外)
// 以下的p、b、c、*p、s、s[0]都是典型左值
int* p = new int(0); // p是左值:变量名,可取地址(&p),有持久存储
int b = 1; // b是左值:普通变量,可取地址(&b),可赋值(b=2)
const int c = b; // c是左值:const修饰的变量,可取地址(&c),但不能赋值(c=3非法)
*p = 10; // *p是左值:指针解引用,可取地址(&(*p)),可赋值
string s("11111"); // s是左值:字符串对象,可取地址(&s),可修改内容
s[0] = 'x'; // s[0]是左值:容器元素,可取地址(&s[0]),可修改值
// 验证左值可寻址:以下两行代码均可正常运行
cout << &c << endl; // 输出c的地址(证明c是左值)
cout << (void*)&s[0] << endl; // 输出s[0]的地址(void*避免按字符串解析)
// 右值核心特征:不能取地址、临时存在、仅可出现在赋值号右侧
double x = 1.1, y = 2.2;
// 以下10、x+y、fmin(x,y)、string("11111")都是典型右值
10; // 字面量常量:右值,不可取地址(&10非法)
x + y; // 表达式临时结果:右值,不可取地址(&x+y非法)
fmin(x, y); // 函数返回非引用值:右值,不可取地址(&fmin(x,y)非法)
string("11111"); // 匿名临时对象:右值,不可取地址(&string("11111")非法)
// 以下代码均被注释,因为尝试取右值的地址会编译报错
//cout << &10 << endl; // 错误:字面量10是右值,不可取地址
//cout << &(x+y) << endl; // 错误:表达式结果是右值,不可取地址
//cout << &(fmin(x, y)) << endl;// 错误:函数返回值是右值,不可取地址
//cout << &string("11111") << endl; // 错误:临时对象是右值,不可取地址
return 0;
}
注意: 在 C++ 中,
fmin(x, y)是一个返回两个数中较小值的函数,它的返回值是一个右值。
3.2 左值引⽤和右值引⽤
Type& r1 = x; Type&& rr1 = y;第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
cpp
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;
// 左值引⽤给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// 右值引⽤给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);//fmin()是库函数 返回 x y里面的较小值
string&& rr4 = string("11111");
- 左值引用不能直接引用右值,但是
const左值引用可以引用右值
cpp
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
注意⚠️:
- 右值引用不能直接引用左值,但是右值引用可以引用
move(左值) template <class T> typename remove_reference<T>::type&& move (T&& arg);move是库里面的一个函数模板,本质内部是进行强制类型转换,当然它还涉及一些引用折叠的知识,这个下面会细讲,这里你可以先短暂理解成 他能在这一行把()里的左值临时转化成右值。
cpp
// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
// 1. int&& rrx1 = move(b);
// 核心:move(b)将左值b强制转换为右值类型,rrx1(右值引用)绑定这个"右值化"的b
// 注意:move仅做类型转换,不会移动数据/改变b的内容,b仍可使用但后续最好不要操作
int&& rrx1 = move(b);
// 2. int*&& rrx2 = move(p);
// 核心:p是存储地址的指针变量(左值),move(p)将其转为右值类型
// rrx2是"指向int的指针"的右值引用,绑定这个右值化的指针p
// 本质:右值引用的类型是int*,而非int,仅针对指针变量p做右值转换
int*&& rrx2 = move(p);
// 3. int&& rrx3 = move(*p);
// 核心:*p是指针解引用(左值,指向b的内存),move(*p)将其转为右值类型
// rrx3是int类型的右值引用,绑定这个右值化的*p(即b的数值)
// 注意:*p本质是b的别名,move(*p)等价于move(b)
int&& rrx3 = move(*p);
// 4. string&& rrx4 = move(s);
// 核心:s是string左值对象,move(s)将其转为右值类型
// rrx4是string类型的右值引用,绑定这个右值化的s
// 场景:常用于STL容器/自定义类,配合移动构造/赋值实现"浅拷贝"提升效率
string&& rrx4 = move(s);
// 5. string&& rrx5 = (string&&)s;
// 核心:等价于move(s),手动强制类型转换(C风格),将左值s转为string&&(右值类型)
// 注意:move的底层实现就是类似这样的强制类型转换,只是封装成模板更通用
// 不推荐直接写强制转换,优先用std::move(更规范、可读性更高)
string&& rrx5 = (string&&)s;
- 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量的表达式属性是左值。(可以笼统认为只要是在左边的变量都是左值!!)
cpp
// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
int& r6 = r1;
// int&& rrx6 = rr1; 错误写法 rr1此时属性为左值
int&& rrx6 = move(rr1); //正确写法
- 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中
r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。
3.3 引⽤延⻓⽣命周期
右值引⽤ 可⽤于为临时对象延⻓⽣命周期 ,对象可以修改 ;const 的左值引⽤ 也能延⻓临时对象⽣存期 ,但这些对象**⽆法** 被修改。
cpp
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
std::cout << r3 << '\n'; // 输出"TestTestTest"
return 0;
}
3.4 左值和右值的参数匹配
- C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。
- 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉很怪,等下面了解右值引用的使用场景时,就能体会这样设计的价值了。
cpp
#include <iostream> // 包含cout输出头文件
#include <utility> // 包含std::move的头文件
using namespace std;
// 重载1:接收普通左值引用(仅匹配非const左值)
void f(int& x)
{
std::cout << "左值引用重载 f(" << x << ")\n";
}
// 重载2:接收const左值引用(匹配const左值、右值,是兜底匹配)
void f(const int& x)
{
std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
// 重载3:接收右值引用(仅匹配右值,优先级高于重载2)
void f(int&& x)
{
std::cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1; // 普通左值(非const)
const int ci = 2; // const左值
// 1. 调用f(int&):普通左值匹配左值引用重载(最匹配)
f(i);
// 2. 调用f(const int&):const左值只能匹配const左值引用重载
f(ci);
// 3. 调用f(int&&):字面量3是右值,匹配右值引用重载;若没有该重载,会匹配f(const int&)
f(3);
// 4. 调用f(int&&):std::move(i)将左值i转为右值类型,匹配右值引用重载
f(std::move(i));
// 核心知识点:右值引用变量本身是左值(因为是变量、可寻址)
int&& x = 1; // x是右值引用变量,但x本身是左值
// 5. 调用f(int&):x是左值,匹配普通左值引用重载(重点!)
f(x);
// 6. 调用f(int&&):std::move(x)将x转为右值类型,匹配右值引用重载
f(std::move(x));
return 0;
}
输出:
左值引用重载 f(1)
到 const 的左值引用重载 f(2)
右值引用重载 f(3)
右值引用重载 f(1)
左值引用重载 f(1)
右值引用重载 f(1)
3.5 右值引⽤和移动语义的使⽤场景(重难点)
我们这里会用到的例子是
string类的封装 学到这里我相信这个简单的类问题应该是不大的 建议往下看的时候稍微过一下就行
cpp
namespace bit
{
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);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
void swap(string& tmp)
{
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
}
// 移动构造
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;
}
// s4 = bit::string("yyyyy");
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
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';
}
char& operator[](size_t pos)
{
//assert(pos < _size);
return _str[pos];
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
bool operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
string addStrings(string num1, string num2)
{
string str;
cout << &str << endl;
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());
cout << "******************************" << endl;
return str;
}
}
3.5.1 左值引⽤主要使⽤场景回顾
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,
addStrings函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决(但是拷贝构造函数对于string来说消耗还是太大了!)。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实。
3.5.2 移动构造和移动赋值
- 移动构造函数 是一种构造函数,类似拷贝构造函数 ,移动构造函数要求第一个参数是该类类型的引用 ,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
cpp
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
- 移动赋值 是一个赋值运算符的重载,它跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
cpp
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
- 对于像
string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要"窃取"引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。
cpp
//构造
bit::string s1("xxxxx");
// 拷贝构造
bit::string s2 = s1;
// 构造+移动构造,优化后直接构造
bit::string s3 = bit::string("yyyyy");
// 移动构造
bit::string s4 = move(s1);
3.5.3 右值引⽤和移动语义解决传值返回问题
这一块涉及到了编译器的优化,对于以前刚学习类和对象的时候,这块确实是一个超级大的难点 但是学到这里 我相信这一块大家基础知识应该是十分扎实的 如果实在还是不是很懂 详细的分析可以参考我写的 类和对象(中)和 类和对象(下)的最后一板块 其实这一块本质就是加了个移动构造板块 如果这一块不扎实 博主给个建议! 一定要理清楚什么是 构造 什么是拷贝构造 把这些基本知识理解透彻再去理解!!
1. 右值对象构造,只有拷⻉构造,没有移动构造的场景(当你不写移动构造的时候)
- 图1 展示了
vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
(图1 ⬆️)- linux下可以将下面代码拷贝到test.cpp文件,编译时用
g++ test.cpp -fno-elide-constructors的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。
2.右值对象构造,有拷⻉构造,也有移动构造的场景(优先走移动构造)
图2 展⽰了
vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
(图2 ⬆️)
- 需要注意的是在
vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3 所⽰。
(图3 ⬆️)
3. 右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
- 图4 左边展⽰了
vs2019 debug和g++ test.cpp -fno-elide-constructors关闭优化环境
下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值。- 需要注意的是在
vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名
(图4 ⬆️)
4.右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
- 图5 左边展⽰了
vs2019 debug和g++ test.cpp -fno-elide-constructors关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。- 需要注意的是在
vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
3.5.4 右值引⽤和移动语义在传参中的提效
- 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本

- 当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象
- 当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上
3.6 类型分类
- C++11以后,进一步对类型进行了划分,右值 被划分纯右值 (pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
- 纯右值 是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如:
42、true、nullptr或者类似str.substr(1, 2)、str1 + str2传值返回函数调用,或者整形a、b,a++,a+b等。纯右值和将亡值是C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。 - 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如
move(x)、static_cast<X&&>(x) - 泛左值 (generalized value,简称glvalue),泛左值包含将亡值和左值。
表达式值类别
泛左值 glvalue
右值 rvalue
将亡值 xvalue
纯右值 prvalue
左值 lvalue
核心:泛左值=左值+将亡值;
C++11右值=纯右值+将亡值;
C++98右值≈C++11纯右值
| 类别 | 英文缩写 | 核心特征 | 典型示例(可直接验证) |
|---|---|---|---|
| 左值 | lvalue | 可寻址、有持久存储 | 1. 普通变量:int a=10;(a是左值) 2. 指针解引用:*p 3. 右值引用变量:int&& x=1;(x是左值) 4. const变量:const int b=20; |
| 纯右值 | prvalue | 不可寻址、临时值 | 1. 字面量:10、true、"hello" 2. 表达式结果:a+b、a++ 3. 传值返回函数:fmin(x,y) 4. 匿名临时对象:string("test") |
| 将亡值 | xvalue | 右值引用类型、资源可转移 | 1. std::move(左值):move(a)、move(x) 2. 强制右值转换:static_cast<int&&>(a) 3. 返回右值引用的函数调用 |
| 泛左值 | glvalue | 左值 + 将亡值 | 包含上述"左值"+"将亡值"的所有示例 |
| C++11右值 | - | 纯右值 + 将亡值 | 包含上述"纯右值"+"将亡值"的所有示例 |
3.7 引⽤折叠&&万能引用的引出(重难点)
- C++中不能直接定义引用的引用如
int& && r = i;,这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用。
cpp
typedef int& lref;
typedef int&& rref;// 1
using rref = int&&;// 2
//1 和 2 语意完全相等
- 通过模板或
typedef中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。 - 下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家需要一个一个仔细理解一下。
cpp
// 1. typedef场景
typedef int& LRef;
typedef int&& RRef;
LRef& r1; // int& & → int&(左值引用)
LRef&& r2; // int& && → int&(左值引用)
RRef& r3; // int&& & → int&(左值引用)
RRef&& r4; // int&& && → int&&(右值引用)
| 引用组合类型 | 折叠后最终类型 | 核心判定 |
|---|---|---|
T& &(左值+左值) |
T&(左值引用) |
非"右值+右值"均折叠为左值引用 |
T& &&(左值+右值) |
T&(左值引用) |
|
T&& &(右值+左值) |
T&(左值引用) |
|
T&& &&(右值+右值) |
T&&(右值引用) |
仅该组合折叠为右值引用 |
⚠️:
- 像
func这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
cpp
// 2. 模板场景
template <typename T>
void func(T&& arg) {}
int a = 10;
func(a); // 实参是左值 → T=int& → T&&=int& && → int&(左值引用)
func(10); // 实参是右值 → T=int → T&&=int&&(右值引用)
Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
| 实参类型 | 模板参数 T 推导结果 |
T&& 折叠后类型 |
|---|---|---|
普通左值(如 int a) |
T = int& |
int&(左值引用) |
右值(如 10) |
T = int |
int&&(右值引用) |
const左值(如 const int a) |
T = const int& |
const int&(const左值引用) |
3.8 完美转发&&万能引用的实践(重难点)
问题根源:
万能引用T&&接收的参数(如Function中的t),无论原实参是左 / 右值,t本身都是左值属性 ;直接传递t会丢失原实参 的value category(值类别),只能匹配左值引用重载。
cpp
#include <iostream>
#include <utility> // 包含std::forward/std::move的头文件
using namespace std;
// 4个重载函数:区分不同值类别
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 Function(T&& t)
{
// ========== 场景1:不使用完美转发(默认) ==========
Fun(t); // t是变量,无论原实参是左/右值,t本身都是左值属性
// ========== 场景2:使用完美转发(取消注释测试) ==========
// Fun(forward<T>(t)); // 保留原始实参的左/右值属性
}
int main()
{
cout << "===== 不使用std::forward的输出 =====" << endl;
// 1. 实参是右值(10) → t是左值 → 匹配Fun(int&)
Function(10); // 输出:左值引用
// 2. 实参是左值(a) → t是左值 → 匹配Fun(int&)
int a;
Function(a); // 输出:左值引用
// 3. 实参是右值(move(a)) → t是左值 → 匹配Fun(int&)
Function(std::move(a));// 输出:左值引用
// 4. 实参是const左值(b) → t是左值 → 匹配Fun(const int&)
const int b = 8;
Function(b); // 输出:const 左值引用
// 5. 实参是const右值(move(b)) → t是左值 → 匹配Fun(const int&)
Function(std::move(b));// 输出:const 左值引用
// ========== 取消Function中forward注释后,重新运行 ==========
cout << "\n===== 使用std::forward的输出 =====" << endl;
// 1. 实参是右值(10) → forward保留右值属性 → 匹配Fun(int&&)
// Function(10); // 输出:右值引用
// 2. 实参是左值(a) → forward保留左值属性 → 匹配Fun(int&)
// Function(a); // 输出:左值引用
// 3. 实参是右值(move(a)) → forward保留右值属性 → 匹配Fun(int&&)
// Function(std::move(a));// 输出:右值引用
// 4. 实参是const左值(b) → forward保留const左值属性 → 匹配Fun(const int&)
// Function(b); // 输出:const 左值引用
// 5. 实参是const右值(move(b)) → forward保留const右值属性 → 匹配Fun(const int&&)
// Function(std::move(b));// 输出:const 右值引用
return 0;
}
3.8.1 利用万能引用改造list
cpp
template<class T>
struct list_node
{
list_node* _next;
list_node* _prev;
T _data;
list_node(const T& x)
:_next(nullptr)
,_prev(nullptr)
,_data(x)
{}
list_node(T&& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(move(x))
{}
};
🔁 改变前:两个重载版本(不推荐)
cpp
// 1. 左值版本
void push_back(const T& x) {
insert(end(), x); // 拷贝构造
}
// 2. 右值版本(非万能引用!)
void push_back(T&& x) {
insert(end(), (T&&)x); // 移动构造
}
❌ 问题:
- 代码重复:两个函数体几乎一样;
- 类型限制 :只能接受
T类型或可隐式转为T的类型;- 无法处理派生类、临时表达式等复杂场景;
- 若
T是模板参数(如list<string>),T&&不是万能引用,而是右值引用!⚠️ 关键误区:
在类模板中,
void push_back(T&& x)中的T是已确定的类型 (如string),所以T&&就是string&&------ 纯右值引用,不是万能引用!
✅ 改变后:使用模板 + 万能引用(完美转发)
cpp
template<class X>
void push_back(X&& x)
{
insert(end(), forward<X>(x));
}
template<class X>
void insert(iterator pos, X&& x)
{
Node* newnode = new Node(forward<X>(x)); // 转发给 Node 构造函数
// ... 链表连接
}
✅ 优势:
- 一套代码支持左值、右值、const、volatile、派生类等所有情况;
- 零额外开销(编译期决议,无运行时判断);
- 符合 STL 标准库设计 (如
std::vector::push_back就是这样实现的)。
3.8.2 深度剖析3.8.1 如何做到完美转发?
🔍 核心机制:万能引用 + std::forward
std::forward<X>(x) 的作用
- 保留原始值类别,实现"完美转发";
- 如果
x原本是左值,就转为左值引用;如果是右值,就转为右值引用。
cpp
Node(forward<X>(x));
// → 若 x 是左值:调用 Node(const T&)
// → 若 x 是右值:调用 Node(T&&)(移动构造)
为什么 insert 也要改成模板?
因为 push_back 调用了 insert,如果 insert 只接受 const T& 或 T&&,就会在中间层丢失值类别。
✅ 所以必须让整个调用链都支持完美转发:
push_back(X&&)
→ insert(pos, X&&)
→ Node(forward<X>(x))
这样才能确保:从最外层到最内层,值类别(左/右)始终被保留。
| 方面 | 改变前 | 改变后 |
|---|---|---|
| 代码量 | 2 个重载 | 1 个模板 |
| 功能 | 仅支持 T 类型 |
支持任意可构造 T 的类型(包括派生类、临时对象) |
| 效率 | 左值拷贝,右值移动 | 自动选择最优构造方式 |
| 扩展性 | 差 | 极强(符合泛型编程思想) |
| STL 兼容性 | 否 | 是 |









