目录
[2. initializer_list](#2. initializer_list)
[1. 左值和右值](#1. 左值和右值)
[2. 左值引用和右值引用](#2. 左值引用和右值引用)
[1. 为什么需要移动语义?先看拷贝的代价](#1. 为什么需要移动语义?先看拷贝的代价)
[2. 移动构造函数](#2. 移动构造函数)
[3. 移动赋值函数](#3. 移动赋值函数)
[4. 显式声明:= default / = delete](#4. 显式声明:= default / = delete)
[1. 问题:普通转发会"丢失"右值属性](#1. 问题:普通转发会"丢失"右值属性)
[2. 解决:用 forward 还原左右值属性](#2. 解决:用 forward 还原左右值属性)
[3. forward 为什么能做到?三件套原理](#3. forward 为什么能做到?三件套原理)
[lambda 表达式各部分说明](#lambda 表达式各部分说明)
C++11简介
C++11 是 C++ 发展过程中一次具有里程碑意义的版本更新。相比之前的 C++98标准,C++11 不仅对原有语言特性进行了完善,还加入了大量新的语法特性和标准库组件,使 C++ 在保持高性能、灵活性以及底层控制能力的同时,代码编写也变得更加简洁、高效和现代化。
C++11 能更好地用于系统开发和库开发、语言更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。
一、列表初始化
1.大括号初始化
在c++98中,允许数组 和结构体使用大括号初始化,例如:

C++11想统一初始化方式,试图实现一切对象皆可用**{}** 初始化,{}初始化也叫做列表初始化。
内置类型支持,自定义类型也支持。自定义类型本质是类型转换,中间会产生临时对象(依赖于构造函数),最后优化了以后变成直接构造。
{}初始化的过程中,可以省略掉=。

2. initializer_list

上面的初始化已经很方便了,但是对象容器初始化还是不太方便,比如一个 vector 对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持。所以这是很不方便的。
C++11库中提出了一个 std::initializer_list 的类,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束。
std::initializer_list 支持迭代器遍历。
cpp
#include <iostream>
#include <vector>
#include <list>
#include <map>
using namespace std;
int main()
{
vector<int> v = { 1,2,3,4,5 };
list<double> l = { 3.6,5.9,4.8 };
map<string, int> m = { {"hello", 1}, {"nihao", 3} };
return 0;
}
//这里赋值使用的大括号及其内部元素,即是initializer_list。
二、右值引用和移动语义
1. 左值和右值
左值: 一般是一个持久存在的表达式,有明确内存地址 ,可以进行取地址操作。
右值:指那些临时性的 ,即将被销毁的 ,以及常量表达式,没有明确的内存地址,不能进行取地址操作。

2. 左值引用和右值引用
左值引用是给左值取别名,右值引用是给右值取别名。举个例子:
cpp
int main()
{
int a = 0;
int& r1 = a; //左值引用
int&& r2 = 1; //右值引用
return 0;
}
左值引用不能直接引用右值,需要加上const;右值引用不能直接引用左值,需要将左值使用move函数包装起来(move将左值转化为右值引用)。示例:
cpp
int main()
{
const int& r1 = 1; //左值引用引用右值
int a = 0;
//右值引用引用左值
int&& r2 = (int&&)a; //强转
int&& r3 = std::move(a); //使用move函数
return 0;
}

3.右值引用的的意义
const左值引用和右值引用都可以延长临时对象的生命周期,直至引用的生命周期结束,但const左值引用所引用的对象不可被修改;而通过右值引用,可以修改所引用的临时对象,因为右值引用本身是左值。
cpp
int main()
{
const int& r1 = 1 + 3;
r1++; // 报错,表达式必须是可修改的左值
int&& r2 = 1 + 3;
r2++; // 可以修改
return 0;
}

func1 使用左值引用传参,可以减少对象拷贝,提高效率。func2 也可以返回左值引用,但要注意对象的生命周期:如果引用的是函数内部的局部对象,那么函数执行结束后对象就被销毁了,返回的引用也就失效了,因此这种情况不能使用左值引用返回。

str 是函数内部的局部对象,函数结束后就会被销毁。因此这里不能使用左值引用返回,否则返回的引用会指向一个已经被销毁的对象,形成悬空引用。这里使用按值返回是没有问题的。

局部对象出作用域就会销毁,所以会拷贝构造一个临时对象,然后再拷贝构造给ret。不过vs2022编译器会优化,连续的构造、拷贝构造会优化为直接构造。
4.移动构造和移动赋值
原来 C++ 类中,有 6 个默认成员函数:构造函数,析构函数,拷贝构造函数,拷贝赋值重载,取地址重载,const 取地址重载。
而 C++11 新增了两个:移动构造函数和移动赋值运算符重载。
移动构造的本质是指针转移(把别人的资源"窃"过来),不是再深拷贝一份------适合临时对象(右值),性能提升巨大。
1. 为什么需要移动构造?先看拷贝的代价
cpp
std::string s1 = "一串很长的字符串..."; // 假设底层 char[1000000]
std::string s2 = s1; // 深拷贝:开新空间、复制 100w 字节
std::string s3 = std::move(s1); // 移动:s1 把指针"给"s3,自己置空
// 结果:s1 = "",s2 和 s3 各持一份数据,但 s3 这份是"窃"来的,没花复制成本
2. 移动构造函数
构造函数的一种重载,专门处理"用右值初始化新对象"。
cpp
class String {
private:
char* _str;
size_t _size;
size_t _capacity;
public:
// 移动构造(窃取------把别人指针拿过来)
String(String&& s) { // 第一个参数必须是右值引用 T&&
_str = s._str; // 1. 直接把对方的指针拿过来
_size = s._size;
_capacity = s._capacity;
s._str = nullptr; // 2. 关键一步:把对方置空(防止它析构时把资源也释放掉)
}
~String() { delete[] _str; }
};

3. 移动赋值函数
已存在的对象 = 另一个对象,分两种情况:左值用拷贝赋值(operator=),右值用移动赋值(operator&&)。
cpp
// 移动赋值(参数用值传递,内部 swap,天然异常安全)
String& operator=(String s) { // 形参 s 是实参的拷贝(这里要触发移动构造)
swap(s); // 1. 把 this 的资源和 s 交换
return *this; // 2. s 离开作用域时,析构的是 this 原来的资源
}
// 移动版(C++11 之后常写为更明显的右值引用版本)
String& operator=(String&& s) {
if (this != &s) { // 自赋值检查
delete[] _str; // 先释放自己原有的资源
_str = s._str;
_size = s._size;
_capacity = s._capacity;
s._str = nullptr;
}
return *this;
}
- 你没写 :析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值 中的任何一个 ,编译器就不再自动生成移动构造和移动赋值。
- 反过来:你只 写了其他四个 (析构/拷贝构造/拷贝赋值/普通赋值),没碰 移动相关的,编译器会默认生成移动版本。
为什么这么反直觉?因为移动是"窃资源",如果你的类需要自己写析构(说明有资源要管)或者写拷贝构造(说明深拷贝逻辑特殊),编译器就不敢默认生成移动版本,怕行为对不上。
4. 显式声明:= default / = delete
| 写法 | 含义 |
|---|---|
String(String&&) = default; |
强制编译器生成默认移动构造(即便你写了析构) |
String& operator=(String&&) = default; |
同上,移动赋值 |
String(String&&) = delete; |
显式禁止 移动构造(std::move(s) 编译报错) |
String& operator=(String&&) = delete; |
同上,移动赋值 |
= default 常用于"我虽然写了析构/拷贝,但移动我想要编译器默认行为就够了"的场景;= delete 常用于"我这个类不能被移动(资源特殊)"的禁止场景。
5.引用折叠
C++ 规定引用不能引用引用 (不存在"引用的引用"),所以当模板推导出 T& &&、T&& & 这种"引用套引用"的类型时,编译器要按规则折叠成一个引用 ------这就是引用折叠。

cpp
template <typename T>
void f(T&& x) { }
int a = 10;
f(a); // 传左值 → T 推导为 int& → 参数是 int& && → 折叠为 int&
f(std::move(a)); // 传右值 → T 推导为 int → 参数是 int&& → 保持右值引用
5.完美转发
完美转发 = 保留参数的左/右值属性------传进来是左值就给下家左值,是右值就给下家右值,既不丢也不改。
1. 问题:普通转发会"丢失"右值属性

std::move 把 a 变成了右值,但一旦进入 wrapper,arg 就是一个有名字的变量(C++ 标准:有名字的就是左值),右值属性就丢了。这就是为什么要"完美转发"。
2. 解决:用 forward 还原左右值属性

3. forward 为什么能做到?三件套原理
①模板形参 T&& 是万能引用(forwarding reference)------它能根据实参类型自动推导:
- 传左值
a→T推导为int& - 传右值
10或move(a)→T推导为int
② forward 内部靠引用折叠把 T 还原:
// std::forward 的实现本质(C++11/14简化版)
template <class T>
T&& forward(typename remove_reference<T>::type& arg);
// C++11 完整版还有第二个重载(用于显式 T 时传右值场景)
template <class T>
T&& forward(typename remove_reference<T>::type&& arg);
三、lambda
lambda 是 C++11 引入的匿名函数对象 ,可以写在任何需要"一段可调用逻辑"的地方------编译器在底层会把它翻译成一个带 operator() 的匿名类(即仿函数),捕获的变量变成这个匿名类的成员。
lambda 表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
lambda 表达式各部分说明
- capture‑list:捕捉列表 该列表总是出现在 lambda 函数的开始位置,编译器**根据
[]**来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。 - (parameters):参数列表 与普通函数的参数列表一致,如果不需要参数传递,则可以连同
()一起省略。 - mutable 默认情况下,lambda 函数总是一个
const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。 - ->return‑type:返回值类型 用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体 在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
cpp
#include <iostream>
using namespace std;
int main()
{
//简单的lambda表达式
auto f = [](int x, int y)->int {return x + y; };
//调用
cout << f(1, 2) << endl;
return 0;
}
在定义lambda表达式时,我们要遵守如下规则:
捕捉列表的" "不能省略。
lambda表达式的参数为空时,参数列表(包括"( )")可以省略。
返回值类型(包括"->")可以省略。
若返回值类型已写,那么就算没有参数,参数列表的"( )"也不能省略。
函数体的"{ }"不能省略。
捕捉列表
lambda表达式的函数内部**只能使用参数列表的变量,**这时要想使用外部变量,就需要依靠捕捉列表了。捕捉列表分为三种,显示捕捉,隐式捕捉,混合捕捉。
①显示捕捉
显示捕捉就是在捕捉列表里写自己要使用的变量。
cpp
int main()
{
int a = 1;
int b = 3;
auto f = [a](int x)
{
//return a + b + x; // 报错,b不能使用
return a + x; //捕捉列表中写明了a,可以使用a
};
cout << f(1) << endl;
return 0;
}
这里a的值不能修改,因为是传值捕捉,若要修改就得变成传引用捕捉[&a]了
②隐式捕捉
隐式捕捉就显得十分便利:它的机制是函数体内部使用了哪些变量就捕捉哪些变量。自动捕捉
auto f1 = = {}; // 隐式的传值捕捉
auto f2 = \& {}; // 隐式的传引用捕捉
③混合捕捉
如果我们需要在隐式捕捉的基础上,确保其中某些值可以被修改或不能被修改,那么就可以使用混合捕捉。
cpp
auto f1 = [=, &a] {}; // 其他变量使用传值捕捉,a使用传引用捕捉
auto f2 = [&, a] {}; // 其他变量使用传引用捕捉,a使用传值捕捉
混合捕捉列表的第一个位置必须是=或&。
捕捉列表的其他要点
-
全局变量和静态变量不需要,也不能通过捕捉列表进行捕捉。
Lambda 表达式可以在作用域允许的情况下直接访问它们。
-
对于值捕捉,Lambda 会将外部变量的值复制一份,保存到 Lambda 对象内部。
默认情况下,这些通过值捕捉得到的成员不能在 Lambda 函数体内修改。
如果希望修改 Lambda 内部保存的副本,同时不影响外部原变量 ,可以使用
mutable关键字。 -

cpp
int main()
{
int a = 0;
auto f = [=]() mutable -> int
{
a++; // 修改的是 Lambda 内部保存的 a 的副本
return a;
};
cout << f() << endl; // 1
cout << a << endl; // 0,外部的 a 没有改变
return 0;
}
注意:在 C++11~C++20 中,使用 mutable 时,即使 Lambda 没有参数,也需要写出空参数列表 ()。