
C++专栏:C++_Yupureki的博客-CSDN博客
目录
[1. 列表初始化](#1. 列表初始化)
[2. 右值引用与移动语义](#2. 右值引用与移动语义)
[2.1 左值与右值](#2.1 左值与右值)
[2.2 右值引用](#2.2 右值引用)
[2.3 引用延长生命周期](#2.3 引用延长生命周期)
[2.4 左值与右值的参数匹配](#2.4 左值与右值的参数匹配)
[2.5 移动构造与移动赋值](#2.5 移动构造与移动赋值)
[2.6 类型分类](#2.6 类型分类)
[2.7 引用折叠](#2.7 引用折叠)
[2.8 完美转发](#2.8 完美转发)
[3. 可变参数模板](#3. 可变参数模板)
[3.1 基本语法](#3.1 基本语法)
[3.2 包扩展](#3.2 包扩展)
[4.3 empalce系列接口](#4.3 empalce系列接口)
[4. 新的类功能](#4. 新的类功能)
[4.1 默认的移动构造和移动赋值](#4.1 默认的移动构造和移动赋值)
[4.2 defult和delete](#4.2 defult和delete)
[4.3 final与override](#4.3 final与override)
[5. Lambda](#5. Lambda)
[5.1 捕捉列表](#5.1 捕捉列表)
[6. 包装器](#6. 包装器)
[6.1 function](#6.1 function)
[6.2 bind](#6.2 bind)
上一篇:从零开始的C++学习生活 15:哈希表的使用和封装unordered_map/set-CSDN博客
前言
我们之前所讲的大部分都是C++98的内容。由于语言版本迭代与编译器支持的跟进问题,并且考虑到内容的更新内容的含金量,我们对于一款编程语言新版本的学习始终保观察态度,但C++11是我们必须所学的内容。
C++11 是自 C++98 以来最重要的一次更新,它不仅引入了大量现代化特性,还极大地提升了开发效率和代码性能。从 2011 年发布至今,C++11 已成为现代 C++ 开发的基石。我将系统介绍 C++11 的核心特性,包括列表初始化、右值引用与移动语义、可变参数模板、lambda 表达式、包装器等,并附上代码示例,帮助你快速掌握这些内容。

1. 列表初始化
C++11 引入了统一的列表初始化语法,使用 {} 来初始化任何类型的对象,包括内置类型、自定义类型和容器。
在C++98中我们一般只能对结构体和数组进行{}初始化
cpp
struct Point {
int x, y;
};
int main() {
// C++98 方式
Point p1 = {1, 2};
int arr1[] = {1, 2, 3};
}
但在C++11以后想统⼀初始化方式,试图实现⼀切对象皆可用{}初始化,{}初始化也叫做列表初始化。并且{}初始化的过程中,可以省略掉=
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化 了以后变成直接构造。
cpp
int main{
// C++11 方式
Point p2{1, 2};
int arr2[]{1, 2, 3};
std::vector<int> vec = {1, 2, 3, 4, 5};
}
std::initializer_list
上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如⼀个vector对象,我想用N个
值去构造初始化,那么我们得实现很多个构造函数才能支持
C++11库中提出了⼀个std::initializer_list的类, 这个类的本质是底层开⼀个数组,将数据拷贝
过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
容器类(如 vector、map)支持使用 std::initializer_list 进行初始化,使得多元素初始化更加简洁
cpp
std::vector<int> v = {1, 2, 3};
std::map<std::string, int> m = {{"Alice", 1}, {"Bob", 2}};
2. 右值引用与移动语义
2.1 左值与右值
-
左值:有名称、有地址的对象(如变量)。
-
右值 :临时对象、字面量等(如
10、x + 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';
cout << &c << endl;
cout << (void*)&s[0] << endl;
右值一般是字面值常量或者临时变量,因此右值一般声明周期仅存在于这一行,不存在地址。只能出现在=右边
cpp
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
2.2 右值引用
Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引用,左值引用就是给左值取别
名,第⼆个就是右值引用,同样的道理,右值引用就是给右值取别名。
左值引用不能直接引用右值,因为右值具有常属性,除非使用const修饰
右值引用不能直接引用左值,但是可以引用move(左值)的返回值
这里的move为C++库里面定义的一个返回值,其本质是强制类型转换,即把左值强转成右值
需要注意的是,例如
std::string&& s = std::string("hello");
虽然=右边的是临时对象,因此是右值,但是s本身不是右值而是左值
因此右值引用的变量只是引用的是右值而已,但其本身还是左值
2.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';
return 0;
}
2.4 左值与右值的参数匹配
C++中可以重载函数。我们知道,编译器选定函数一定是选定最匹配的。
例如const对象一定只能选定传const的函数
普通左值对象可以传const和不带const的参数,但是如果二选一还是优先不带const,除非没有这种函数
右值对象可以传带const的或者右值引用的参数,但还是优先右值引用
cpp
void f(int& x)
{
std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引⽤重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调⽤ f(int&)
f(ci); // 调⽤ f(const int&)
f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
f(std::move(i)); // 调⽤ f(int&&)
int&& x = 1;
f(x); // 调⽤ f(int& x)
f(std::move(x)); // 调⽤ f(int&& x)
return 0;
}
2.5 移动构造与移动赋值
移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引
⽤,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函
数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
cpp
class String {
public:
String(String&& other) { // 移动构造
data_ = other.data_;
other.data_ = nullptr;
}
String& operator=(String&& other) { // 移动赋值
if (this != &other) {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
}
return *this;
}
};
移动构造和移动赋值都是对于右值而言,并且都是浅拷贝,我们使用浅拷贝就相当于掠夺其资源。
因为我们传的右值一般都是临时对象,临时对象的声明周期只有这一行,我们以后不会也不能使用这个临时对象的资源,那么把他的资源抢过来也无伤大雅,还能大幅提高效率。
这就相当于遗产一样,人过世之后其财产自然对其而言也没用了,自然需要继承遗产的人
2.6 类型分类
C++11以后,进⼀步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值
(expiring value,简称xvalue)。
纯右值是指那些字面值常量或求值结果相当于字面值或是⼀个不具名的临时对象。 C++11中的纯右值概念划分等价于 C++98中的右值。
将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如
move(x) 、 static_cast<X&&>(x)
泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

2.7 引用折叠
C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错。但剋通过模板 或typedef
中的类型操作可以构成引用的引用。
cpp
typedef int& lref;
typedef int&& rref;
cpp
template<class T>
void f1(T& x)
{}
template<class T>
void f2(T&& x)
{}
通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规
则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
cpp
//参数为左值引用时,无论传的是左值还是右值,x都引用的都是左值
template<class T>
void f1(T& x)
{}
int main()
{
int i = 1;
f1(i);
f1(1);//x引用的都是左值
return 0;
}
cpp
//参数为右值引用时,只有传递的是右值,x才是右值引用
template<class T>
void f2(T&& x)
{}
int main()
{
int i = 1;
f1(i);//左值引用
f1(1);//右值引用
return 0;
}
像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左
值时就是左值引用,传递右值时就是右值引用,因此f2既可以传左值也可以传右值。有些地方也把这种函数模板的参数叫做万能引用。
2.8 完美转发
cpp
void f(int& x)
{
std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引⽤重载 f(" << x << ")\n";
}
template<class T>
void f2(T&& x)//无论传的是左值还是右值,x本身永远为左值
{
f(forward<T>(x))//x如果是右值引用就调用f(int&& x)函数
}
int main()
{
int i = 1;
f1(i);//左值引用
f1(1);//右值引用
return 0;
}
我们使用万能引用的目的是为了让x即可以是左值引用又可以是右值引用,那么x传递的对象就可以是右值。但是x本身是左值,用x传递函数永远都是调用接受左值的函数。
因此我们可以使用 forward<T>(x)来保持x引用对象的属性
如果x本来就是左值引用,那么不变。
如果x是右值引用,那么传递的就是右值了
3. 可变参数模板
C++11 支持可变参数模板,允许函数或类接受任意数量和类型的参数。
3.1 基本语法
cpp
template<typename... Args>
void print(Args... args) {
// 使用 sizeof...(args) 获取参数个数
std::cout << sizeof...(args) << std::endl;
}
这里我们可以使用sizeof...运算符去计算参数包中参数的个数。
我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或
typename...指出接下来的参数表示零或多个类型列表
在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表
原来的函数模板只能自定义参数的类型,但可变参数模板可以自定义参数的个。因此可以看作是模板的模板
同时参数类型既可以是左值引用也可以是右值引用
cpp
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
cpp
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("xxxxx")); // 包⾥有2个参数
Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
return 0;
}
3.2 包扩展
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它
当扩展⼀个 包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
cpp
void ShowList()
{
// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
// args是N个参数的参数包
// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1, string("xxxxx"), 2.2);
return 0;
}
args的展开是自动展开的,是编译器干的事,我们无需插手。当展开了一个参数后,紧接着会展开第二个参数。最后没有参数时,就要调用无参的函数,也是我们制造一个无参showlist的原因
4.3 empalce系列接口
cpp
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position,
Args&&... args);
C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上
兼容push和insert系列并且更加高效
cpp
int main()
{
list<pair<string,int>> lt;
lt.push_back({"苹果",1});
lt.emplace_back("苹果,1);
}
我们使用push_back需要先创造一个临时对象才能插入。但是emplace由于支持可变参数,我们可以直接按照顺序插入元素,跳过了临时对象创建这一过程
4. 新的类功能
4.1 默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重
载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器
会生成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意⼀
个。那么编译器会自动生成⼀个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执
行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用
移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意⼀个,那么编译器会自动生成一个默认移动赋值。
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
4.2 defult和delete
假设你要使用某个默认的函数,但是因为⼀些原因 这个函数没有默认⽣成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已,
这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
Person(Person&& p) = default;
//Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
4.3 final与override
final用于继承当中,意思为该类无法被继承
override用于修饰派生类的虚函数,强制检查是否重写基类的虚函数
这些我们在之前讲过,在此不再过多赘述
5. Lambda
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使用层而言没有类型,所以我们⼀般是用auto或者模板参数定义的对象去接
收 lambda 对象。
cpp
[capture](parameters) -> return_type { body }
cpp
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
(int x,int y)其实就是函数的参数
->int则是函数的返回值类型,也可以不写让系统自己去推
{}里面则是函数体了
5.1 捕捉列表
\]是lambda的捕捉列表
lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就
需要进行捕捉
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。\[x,
y, \&z\] 表示x和y值捕捉,z引用捕捉。
第⼆种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表
写⼀个\&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些
变量。
总得来说
* `[=]`:值捕获
* `[&]`:引用捕获
* `[a, &b]`:混合捕获
```cpp
int main()
{
// 只能⽤当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << func1() << endl;
// 隐式值捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
}
```
### 6. 包装器
#### 6.1 function
std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存
储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对
象被称为 std::function 的目 *标* 。
简单点来讲,其实包装器就类似于之前的函数指针,只不过包装器不仅能实现函数指针的作用,只要能够调用,就可以使用包装器
```cpp
std::function