C++11 深度解析:重塑现代 C++ 的关键特性
本文深入解析 C++11 标准的核心特性,涵盖列表初始化、右值引用与移动语义、可变参数模板、Lambda 表达式及包装器(std::function/bind)。文章通过对比 C++98 与 C++11 的差异,阐述了新特性如何解决旧版痛点,如统一初始化语法、优化临时对象拷贝性能、支持变长参数及简化匿名函数定义。内容包含详细代码示例与原理分析,旨在帮助开发者掌握现代 C++ 编程范式,提升代码效率与可维护性。

本文深入解析 C++11 标准的核心特性,涵盖列表初始化、右值引用与移动语义、可变参数模板、Lambda 表达式及包装器(std::function/bind)。文章通过对比 C++98 与 C++11 的差异,阐述了新特性如何解决旧版痛点,如统一初始化语法、优化临时对象拷贝性能、支持变长参数及简化匿名函数定义。内容包含详细代码示例与原理分析,旨在帮助开发者掌握现代 C++ 编程范式,提升代码效率与可维护性。


作为 C++ 开发者,C++11 是必须掌握的里程碑版本。它不仅解决了 C++98/03 时代的诸多痛点,更奠定了现代 C++ 的编程范式。本文系统梳理 C++11 的核心特性。
我们知道一门语言不是一成不变的,它会不断的更新。对于 C++ 来说,第一次重要的版本更新是在 1998 年推出的 C++98。C++98 作为 C++ 的第一个正式国际标准(ISO/IEC 14882:1998),奠定了现代 C++ 最核心的基础框架。后续所有版本(C++03 到 C++23)的演进,都是在这个基础上的扩展、优化和补充。
C++ 的发展以 ISO 国际标准为核心节点,每个版本都针对性解决前一阶段的痛点,逐步迈向'现代 C++':

C++11 并非一蹴而就,它最初被称为'C++0x'。直到 2011 年 8 月 12 日,ISO 才正式采纳这一标准,命名为'C++11'。它与前一版本 C++03 间隔了 8 年,是迄今为止 C++ 版本迭代中间隔最长的一次,也正因如此,它凝聚了大量关键改进:标准化了当时已有的实践(如 STL 的进一步优化),同时引入了全新的抽象机制(如移动语义、lambda)。
从 C++11 开始,C++ 进入了'三年一更新'的规律迭代周期(C++14、C++17、C++20、C++23 依次发布),而 C++11 正是这一切的起点。
在 C++98 中,对象初始化的语法堪称'混乱':数组用{},结构体用{},但类对象却只能用构造函数 +()或=,这种不一致性让开发者频繁查阅文档。而 C++11 的列表初始化(又称{}初始化)解决了这个问题,核心目标是'一切对象皆可通过{}初始化'。
C++98 仅支持数组和结构体的{}初始化,例如:
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;
}
但如果是自定义类(如 Date),就无法用{}初始化 —— 这在 C++11 中被彻底改变。
C++11 的列表初始化的关键在于支持所有对象使用{}进行初始化:不管是内置类型(int x {2};)还是自定义类型(Date d {2024, 10, 1};)均适用;并且使用列表初始化时可以不加=。
下面让我们通过代码来感受一下:
#include <iostream>
#include <vector>
using namespace std;
struct Point { int _x; int _y; };
class Date {
public:
//直接构造
Date(int year = 1, int month = 1, int day = 1) :_year(year), _month(month), _day(day) { cout << "Date(int year, int month, int day)" << endl; }
///拷贝构造
Date(const Date& d) :_year(d._year), _month(d._month), _day(d._day) { cout << "Date(const Date& d)" << endl; }
private:
int _year;
int _month;
int _day;
};
实际上使用列表初始化的本质是创建一个临时对象,然后临时对象去进行拷贝构造。
int main() {
Date d1 = {2025, 1, 1};
const Date& d2 = {2024, 7, 25};
return 0;
}
就像上述代码中的 d1,他的本质是用{ 2025, 1, 1}构造一个 Date 的临时对象,然后临时对象再去拷贝构造 d1,不过有的编译器会对这种'直接构造 + 拷贝构造'的形式进行优化,优化成直接构造:

因为使用列表初始化会创建一个临时对象,因此这里 d2 引用的是{ 2024, 7, 25 }构造的临时对象,因此我们可以看到我们 d2虽然是一个引用的别名,但 Date 还是进行了构造。
需要注意的是 C++98 支持单参数时的类型转换,也就是说当参数只有一个时也可以不用
{}进行初始化,例如下面的方式在 C++98 中是被支持的:Date d3 = 2025;
在使用列表初始化时是可以省略=的:
int main() {
Point p1 {1, 2};
int x1 {2};
Date d4 {2024, 7, 25};
const Date& d5 {2024, 7, 25};
return 0;
}
只有使用列表初始化时才可以省略等号,例如下面这种方式是不可以的:
Date d6 2025;
C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下还可以带来不少便利,例如在容器中 push/insert多参数构造的对象时,使用{}初始化会很方便:
int main() {
vector<Date> v;
//有名对象
Date d7 = {2024, 7, 25};
v.push_back(d7);
//匿名对象
v.push_back(Date(2025, 1, 1));
//比起有名对象和匿名对象传参,这里{}更有性价比
v.push_back({2025, 1, 1});
return 0;
}
有了列表初始化后,我们对于需要使用多参数构造的对象传参时便不用像之前一样只能使用有名对象和匿名对象传参,而是直接使用列表初始化,可以简化我们的代码。
列表初始化的另一大贡献,是引入了std::initializer_list类模板 —— 它让 STL 容器的初始化变得前所未有的简单。上面的初始化已经很方便了,但是对于对象容器初始化还是不太方便,比如一个 vector对象,在 C++98 中,要初始化他,你可能需要这样写:
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
当我们想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持,例如我们想要实现下面两种构造场景:
vector<int> v1 = {1, 2, 3};
vector<int> v2 = {1, 2, 3, 4, 5};
我们就需要写这两种不同参数所对应的构造函数。而 C++11 借助std::initializer_list,直接支持'字面量式'初始化,也就是说我们上述两种方式的初始化都可以支持。他的本质是 STL 容器(如 vector、map)在 C++11 中新增了接收std::initializer_list的构造函数,std::initializer_list也叫做初始化列表,他是 C++11 新增的类,这个类的本质是底层开一个数组,将数据拷贝,std::initializer_list内部有两个指针分别指向数组的开始和结束。
auto il = {10, 20, 30};// the type of il is an initializer_list
这是他的文档:initializer_list,感兴趣的可以自行查看。std::initializer_list支持迭代器遍历。
容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的{x1,x2,x3...}进行初始化。STL 中的容器支持任意多个值构成的{x1,x2,x3...}进行初始化,就是通过容器中参数为std::initializer_list的构造函数支持的。
另外,容器的赋值也支持
initializer_list的版本:
下面让我们通过具体的代码来认识一下 initializer_list:
#include <iostream>
using namespace std;
int main() {
std::initializer_list<int> mylist;
mylist = {10, 20, 30};
cout << sizeof(mylist) << endl;// 这里 begin 和 end 返回的值是 initializer_list 对象中存的两个指针
//这两个指针的值跟 i 的地址跟接近,说明数组存在栈上
int i = 0;
cout << mylist.begin() << endl;
cout << mylist.end() << endl;
cout << &i << endl;
return 0;
}
下面是运行结果:

有了 initializer_list,我们对于容器的初始化就变得简单了:
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;
int main() {
// {}列表中可以有任意多个值
//这两个写法语义上还是有差别的,第一个 v1 是直接构造
//第二个 v2 是构造临时对象 + 临时对象拷贝优化为直接构造
vector<int> v1({1, 2, 3, 4, 5});
vector<int> v2 = {1, 2, 3, 4, 5};
const vector<int>& v3 = {1, 2, 3, 4, 5};
//这里是 pair 对象的{}初始化和 map 的 initializer_list 构造结合到一起用了
map<string, string> dict = {{"sort","排序"},{"string","字符串"}};
return 0;
}
如果说列表初始化解决了'语法优雅'问题,那右值引用与移动语义就是 C++11 解决'性能瓶颈'的核心 —— 它针对'临时对象的冗余拷贝'这一痛点,提供了根本性的优化方案。C++98 的语法中就有引用的语法,而 C++11 中新增了右值引用语法特性,在 C++98 时的引用,我们称之为左值引用。无论左值引用还是右值引用,都是给对象取别名。
在了解右值引用和移动语义之前,我们首先要了解左值和右值的概念:
右值:临时对象、字面值(如10、3.14)或表达式结果(如a + b),无持久内存地址,不能出现在赋值符号左边;以下几个 10、x + y、fmin(x, y)、string("11111")都是常见的右值
double x = 1.1, y = 2.2;
10;
x + y;
fmin(x, y);
string("11111");
左值:有明确内存地址、可长期存在的对象(如变量、解引用的指针),能出现在赋值符号左边;以下的 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';
简单来说,如果对一个可以取它的地址,那么它就是左值,反之则是右值。
值得一提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代 C++ 中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
顾名思义,对左值进行引用叫做左值引用,那么右值引用就是对右值进行引用。
C++11 新增右值引用(语法:Type&&),专门用于绑定右值(临时对象),并延长其生命周期。与之对应,之前的引用(Type&)被称为'左值引用'。
Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
对于左值引用和右值引用需要注意下面几个点:
引用可以延长变量的生命周期。右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。
int main() {
std::string s1 = "Test";
// std::string&& r1 = s1; // error:不能绑定到左值
const std::string& r2 = s1 + s1;// correct:到 const 的左值引用延长生存期
// r2 += "Test"; // error:不能通过到 const 的引用修改
std::string&& r3 = s1 + s1;// correct:右值引用延长生存期
r3 += "Test";// correct:能通过到非 const 的引用修改
std::cout << r3 << '\n';
return 0;
}

变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量本身的属性是左值(因为它有内存地址)。
int&& rr1 = 10;
int&& rr2 = rr1;//error: rr1 的属性是左值,不能被右值引用
int&& rr3 = move(rr1);//correct: 如果想用右值引用引用左值,可以用 move 将左值强转为右值
int& r4 = rr1;//correct
右值引用不能直接引用左值,但是右值引用可以引用 std::move(左值)。(std::move()是库中的一个函数模板,本质内部是进行强制类型转换,不移动任何数据)
int a = 10;
int&& rr1 = a;//error: a 是左值,不能被右值引用
int&& rr2 = move(a);//correct
左值引用不能直接引用右值,但是 const左值引用可以引用右值。
int& r1 = 10;//error: 10 是右值,不能用左值引用去引用右值
const int& r2 = 10;//correct
语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看上面代码中 r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要杂糅到一起理解,互相佐证,这样反而是陷入迷途。
在了解右值引用的使用场景之前,让我们先来看一看左值和右值的参数匹配以及右值具体的类型分类,以便我们能更好的去理解右值引用的使用场景。
在之前我们实现一个 const左值引用作为参数的 func函数,那么实参传递左值和右值都可以与之匹配,那么在 C++11 之后我们分别重载左值引用、const左值引用、右值引用作为形参的 func函数,那么实参是左值会匹配func(左值引用),实参是 const左值会匹配func(const 左值引用),实参是右值会匹配func(右值引用)。这是因为我们在调用函数时编译器会去寻找最匹配的函数,下面让我们通过代码来看一下:
#include <iostream>
using namespace std;
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;
}
下面是代码的运行结果:

C++11 以后,进一步对类型进行了划分,右值被划分纯右值 (pure value,简称prvalue)和将亡值 (expiring value,简称xvalue):
string("hello"))、字面量(如123)等。std::move转换的对象)。纯右值和将亡值是在 C++11 中提出的,C++11 中的纯右值概念划分等价于 C++98 中的右值。此外,在 C++11 还有泛左值 (generalized value,简称glvalue),泛左值的核心特征是可以通过地址识别(即具有'身份'),不管该表达式是否可修改。泛左值包含将亡值和左值。可以看到泛左值和右值之间有些部分是重合的,它们之间的关系如下图所示:

这是关于 C++值类型的文档,有兴趣的可以了解细节。
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面的 addStrings 和 generate函数:

C++98 中的解决方案只能是被迫使用输出型参数解决。那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。虽然无法使用右值引用返回解决这个问题,但是我们可以使用右值引用来减少传值返回时的开支,这也就是右值引用的最终目的,就是是实现移动语义—— 对于需要深拷贝的类(如string、vector),移动构造 / 赋值会'窃取'右值对象的资源(如内存缓冲区),而非重新分配内存并拷贝数据,从而大幅提升性能。
**移动构造:**移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。以 string类为例,C++98 中拷贝构造是这样的:
// 拷贝构造:深拷贝,开销大
string(const string& s) {
_str = new char[s._size + 1];
strcpy(_str, s._str);
_size = s._size;
}
而 C++11 的移动构造则'窃取'资源:
// 移动构造:直接交换指针,无拷贝
string(string&& s) noexcept {
swap(_str, s._str);// 窃取 s 的内存缓冲区
swap(_size, s._size);// s 的资源被'掏空',析构时不会影响当前对象
}
对于那些临时对象,虽然我们无法取它们的地址,但它们是占据了内存中的空间,那我们移动语义的目的就是说:当我们要用一个临时对象去创建一个对象,那么我们就不需要再开辟一块空间,而是直接去掠夺临时对象的空间,因为这些临时对象的声明周期只在它自己的当前行,以此做到物尽其用。
对于像 string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要'窃取'引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。
由于右值引用是在 C++11 才被提出,那么在此之前编译器为了提高拷贝效率,它会自动识别,去优化拷贝的过程,如下图所示:
可以看到,对于上述代码,函数返回时本应该有两次拷贝构造,但被编译器优化为两次构造。那么在有了移动构造之后也是一样,编译器也会做出相应的优化:
需要注意的是在 vs2019 的
release和 vs2022 的debug和release版本下,上面的代码优化会非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图所示:这时可能有人会说既然编译器可以自己优化,那么移动语义还有什么用呢?虽然说编译器会对这些相应的场景进行优化,但是我们无法确定使用的编译器是否一定会进行优化,但是如果我们使用了移动语义,无论编译器是否优化,效率都会高于没有移动语义的场景。
查看 STL 文档我们可以发现 C++11 以后容器的 push和 insert等系列的接口都增加了右值引用版本:

右值引用通过移动语义消除冗余拷贝,同时为'拷贝'与'移动'提供明确的语义区分,支撑了标准库的性能优化和模板编程的灵活性。它让 C++ 在保持零成本抽象的同时,大幅提升了处理大型对象和临时对象时的效率,是现代 C++ 性能优化的基石。
C++ 中不能直接定义引用的引用如 int& && r = i;,这样写会直接报错,但是我们可以通过模板推导、auto类型推导、typedef/using别名定义等构成引用的引用。当我们通过这些操作构成引用的引用时,C++11 给出了一个引用折叠的规则:**右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。**具体逻辑如下:
T& & → 折叠为 T&(左值引用的左值引用 → 左值引用)T& && → 折叠为 T&(左值引用的右值引用 → 左值引用)T&& & → 折叠为 T&(右值引用的左值引用 → 左值引用)T&& && → 折叠为 T&&(右值引用的右值引用 → 右值引用)下面的程序中很好的展示了模板和 typedef构成引用时的引用折叠规则:
// 由于引用折叠限定,f1 实例化以后总是一个左值引用
template<class T> void f1(T& x) {}
// 由于引用折叠限定,f2 实例化后可以是左值引用,也可以是右值引用
template<class T> void f2(T&& x) {}
int main() {
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n;// r1 的类型是 int&
lref&& r2 = n;// r2 的类型是 int&
rref& r3 = n;// r3 的类型是 int&
rref&& r4 = 1;// r4 的类型是 int&&
// 没有折叠->实例化为 void f1(int& x) f1<int>(n); f1<int>(0);// error: 指定了类型后推导出是参数是左值引用,左值引用无法引用右值,下面也都同理
// 折叠->实例化为 void f1(int& x) f1<int&>(n); f1<int&>(0);// error
// 折叠->实例化为 void f1(int& x) f1<int&&>(n); f1<int&&>(0);// error
// 折叠->实例化为 void f1(const int& x) f1<const int&>(n); f1<const int&>(0);// 折叠->实例化为 void f1(const int& x) f1<const int&&>(n); f1<const int&&>(0);
// 没有折叠->实例化为 void f2(int&& x) f2<int>(n);// error f2<int>(0);
// 折叠->实例化为 void f2(int& x) f2<int&>(n); f2<int&>(0);// error
// 折叠->实例化为 void f2(int&& x) f2<int&&>(n);// error f2<int&&>(0);
return 0;
}
像 f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,因此这种函数模板的参数也叫做万能引用。
对于万能引用,我们可以参考下面的代码:
template<class T> void Function(T&& t) {
int a = 0;
T x = a;//x++; cout <<&a << endl; cout <<&x << endl << endl;
}
int main() {
// 10 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(10);
int a;
// a 是左值,推导出 T 为 int&,引用折叠,模板实例化为 void Function(int& t)
Function(a);
// std::move(a) 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(std::move(a));
const int b = 8;
// a 是左值,推导出 T 为 const int&,引用折叠,模板实例化为 void Function(const int& t)
// 所以 Function 内部会编译报错,x 不能++
Function(b);
// std::move(b) 是右值,推导出 T 为 const int,模板实例化为 void Function(const int&& t)
// 所以 Function 内部会编译报错,x 不能++
Function(std::move(b));
return 0;
}
Function(T&& t)函数模板程序中,假设实参是 int 右值,模板参数 T的推导为int,实参是 int左值,模板参数 T的推导为int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的 Function,这就是万能引用。
在上面的 Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的 Function函数,传右值实例化以后是右值引用的 Function函数。但是我们知道变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说 Function函数中t的属性是左值,那么我们把t传递给下一层函数 Fun,那么匹配的都是左值引用版本的 Fun函数。如下面代码所示:
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) {
Fun(t);
}
int main() {
// 10 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(10); // 右值
int a;
// a 是左值,推导出 T 为 int&,引用折叠,模板实例化为 void Function(int& t)
Function(a); // 左值
// std::move(a) 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a 是左值,推导出 T 为 const int&,引用折叠,模板实例化为 void Function(const int&t)
Function(b); // const 左值
(std::(b));
;
}

这里我们想要保持 t对象的属性,就需要使用完美转发实现。完美转发的本质是一个函数模板 forward,他主要还是通过引用折叠的方式实现,上面示例中传递给 Function的实参是右值,T 被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给 Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回
template<class T> void Function(T&& t) {
Fun(forward<T>(t));
}
那么我们在运行之前的代码来看一下结果:

这样结果就符合我们的预期了。
完美转发(Perfect Forwarding)是 C++ 中用于在函数调用链中保留参数原始值类别(左值 / 右值) 的技术,其核心用途是解决'参数转发时值类别丢失'的问题,确保转发后的参数能被目标函数以正确的方式(左值引用接收左值,右值引用接收右值)处理。
C++11 支持可变参数模板,允许模板接收'零或多个参数'(称为'参数包'),解决了之前模板无法处理不定数参数的问题。
C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:
语法:template <class... Args>
我们用省略号来指出一个模板参数或函数参数表示一个包:
class...或typename...指出接下来的参数表示零或多个类型列表;函数参数包可以用左值引用或右值引用表示,跟前普通模板一样,每个参数实例化时遵循引用折叠规则。如下所示:
template<class... Args> void Func(Args... args) {}
template<class... Args> void Func(Args&... args) {//左值引用
template<class... Args> void Func(Args&&... args) {//万能引用
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。我们看下面的代码:
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;
}
这里我们可以使用
sizeof...运算符去计算参数包中参数的个数。
下面让我们来看一下运行结果:

可以看出与我们的预期相符。上面我们说了,可变参数模板的本质与模板相同,都是去实例化对于的函数,对于上面的 Print 函数,编译本质这里会结合引用折叠规则实例化出以下四个函数:
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
如果没有可变参数模板,我们需要实现出多个函数模板才能支持这里的功能,而有了可变参数模板,我们进一步被解放,他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
**普通函数模板:**本来要写多个函数->一个函数模板即可 **可变参数函数模板:**本来要写多个函数模板->一个可变参数函数模板即可
对于一个参数包,我们除了能计算他的参数个数,还能扩展它。当一个包时,我们需要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号 (…) 来触发扩展操作。如下面代码所示:
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;
}
我们来看运行结果:

可以看到我们将每一个参数都打印了出来,具体的底层原理如下图所示:

需要注意的是,我们在扩展参数包时,编译递归的终止条件必须重载相应函数对应参数包为 0 的参数,而不能直接在函数中判断参数包是否为 0 来作为递归结束条件,如下列代码是错误的:
template<class t, class... Args> void ShowList(t x, Args... args) {
if (sizeof...(args) == 0) {
return;
}
cout << x << " ";
// args 是 n 个参数的参数包
// 调用 showlist,参数包的第一传给 x,剩下 n-1 传给第二个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template<class... Args> void print(Args... args) {
ShowList(args...);
}
int main() {
print(1, string("xxxxx"), 2.2);
return 0;
}
C++ 还支持更复杂的包扩展:直接将参数包依次展开依次作为一个实参给一个函数去处理。如下面代码所示:
template<class T> const T& GetArg(const T& x) {
cout << x << " ";
return x;
}
template<class... Args> void Arguments(Args... args) {}
template<class... Args> void Print(Args... args) {
// 注意 GetArg 必须返回或者到的对象,这样才能组成参数包给 Arguments
Arguments(GetArg(args)...);
}
int main() {
Print(1, string("xxxxx"), 2.2);
return 0;
}
本质可以理解为编译器编译时,包的扩展模式将上面的函数模板扩展实例化为下面的函数:
void Print(int x, string y, double z) {
Arguments(GetArg(x), GetArg(y), GetArg(z));
}
C++11 以后 STL 容器新增了 emplace系列的接口,emplace系列接口的参数均为可变模板参数,功能上兼容 push和 insert系列。我们以 list 为例,来看一下 emplace系列接口对比 push和 insert系列有哪些优势。

而对于 emplace_back来说,他是一个可变参数函数模板,因此它也是万能引用,而最重要的是它的参数是一个参数包。

对于 push_back来说,它只是一个普通的函数,它的参数在类模板实例化的时候就已经实例化了,value_type就是我们存储在 list中值的类型。因此我们可以看到 push_back有两个版本,分别为左值和右值版本。
就 emplace_back和 push_back而言,当参数的类型与实际的类型相同时是没有区别的,而当参数的类型与实际的参数不同,也就是说需要进行类型转换时,它们的差别就体现出来了,如下图所示:

也就是说当我们使用 emplace系列接口时,参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型 T的构造,这样有些场景会更高效一些,可以直接在容器空间上构造 T对象。而 push_back等系列无法做到,这里 string是单参数,会进行单参数的隐式类型转换,因此两者在此时的使用方法没有太大差别。那么当 list的类型是 pair时呢?
lt1.emplace_back("苹果", 1);
对于 emplace_back,我们直接将 pair的两个参数写入,这样参数包在往下传递时可以直接构造相应的对象。
lt1.push_back({"苹果", 1});
而对于 push_back,我们就无法想上面一样,因为 push_back的参数只有一个,因此我们需要通过列表初始化先构造一个临时对象才能继续往下进行。
emplace系列总体而言是更高效的,因此在平常的使用中更推荐使用 emplace系列去替代 insert和 push系列。
当我们在自定义类中想要实现 emplace 系列接口需要注意在传递参数包过程中,如果是
Args&&... args的参数包,要用完美转发参数包,方式如下std::forward<Args>(args)...,否则编译时包扩展后右值引用变量表达式就变成了左值,导致错误。
可变参数模板的核心是'包扩展',通过 ...将参数包拆解为单个参数。此外,STL 的 emplace_back接口也基于可变参数模板实现,能直接在容器中构造对象(无需临时对象),比 push_back更高效。
在 C++98 的类中,有 6 个默认成员函数,分别是:
最后重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写时编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
其实也很好理解,当我们的类中没有需要深拷贝的资源时,也就不需要写相应的析构函数和拷贝构造等等,那么也就不需要我们再显示的写移动构造,默认生成的浅拷贝就足够了。而如果需要进行深拷贝,那么我们就需要自己去写对应的移动构造等等,因为默认生成的无法满足需求,所以编译器干脆就不生成默认的。
在 C++11 中,新增了 default和 delete两个关键字,它们主要用于显式控制类的特殊成员函数(如构造函数、拷贝控制函数等)的生成行为,解决了 C++98 中'默认函数生成规则不直观''禁止默认函数需用 hack 手段'等问题。
**defult:**显式要求编译器生成默认版本的特殊成员函数.
C++ 类会隐式生成一些特殊成员函数(如默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数等),但这些隐式生成的行为会被用户定义的函数'抑制'。例如:
default关键字的作用是:显式告诉编译器'使用该函数的默认实现',强制编译器生成符合标准的默认版本,避免手动编写冗余代码,同时保证编译器对默认函数的优化。
适用场景与示例:
显式生成默认拷贝控制函数即使未定义其他拷贝函数,也可通过 default明确使用默认版本,增强代码可读性:
class B {
public:
B(const B&) = default;// 显式生成默认拷贝构造函数
B& operator=(const B&) = default;// 显式生成默认拷贝赋值运算符
~B() = default;// 显式生成默认析构函数(编译器通常会默认生成,但显式写出更清晰)
};
注意:
default仅能用于编译器原本会隐式生成的特殊成员函数(如默认构造、拷贝构造、析构等),不能用于普通成员函数。
恢复被抑制的默认构造函数当用户定义了带参构造函数时,默认构造函数会被抑制,此时可用 default显式生成:
class A {
public:
A(int x) :_x(x) {}
// 用户定义了带参构造函数,默认构造被抑制
A() = default;// 显式要求编译器生成默认构造函数(无参)
private:
int _x;
};
**delete:**显式禁止编译器生成特定的特殊成员函数(或普通函数)
在 C++98 中,若要禁止类的拷贝行为(如单例模式),通常的做法是'将拷贝构造函数和拷贝赋值运算符声明为 private且不实现',但这种方式只能在链接期报错(未实现),且不直观。
delete关键字的作用是:显式告诉编译器'禁止生成该函数',若用户尝试使用被 delete的函数,编译器会在编译期直接报错,更安全、更清晰。
适用场景与示例:
禁止特定参数类型的函数重载(包括普通函数)delete不仅可用于特殊成员函数,还能用于普通函数,禁止特定参数的调用(如防止隐式类型转换):
class MyInt {
public:
MyInt(int x) :_x(x) {}
// 禁止从 double 隐式转换为 MyInt(只允许 int 转换)
MyInt(double) = delete;
private:
int _x;
};
int main() {
MyInt a(10); // 正确:int 转换
MyInt b(3.14); // 错误:double 版本被 delete,编译报错
return 0;
}
禁止类的拷贝行为对于不可拷贝的类(如 std::unique_ptr),可通过 delete删除拷贝构造和拷贝赋值:
class Singleton {
public:
// 禁止拷贝构造
Singleton(const Singleton&) = delete;
// 禁止拷贝赋值
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;// 私有默认构造,确保只能通过 getInstance 获取实例
};
此时若尝试拷贝 Singleton对象(如 auto s = Singleton::getInstance();),编译器会直接报错。
总结
default:显式请求编译器生成默认版本的特殊成员函数,解决'用户定义函数抑制默认函数'的问题,代码更简洁且享受编译器优化。delete:显式禁止编译器生成特定函数(包括特殊成员函数和普通函数),编译期检查错误,比 C++98 的'私有不实现'更安全、直观,常用于禁止拷贝或特定类型转换。在 C++11 中对于类还有其他的功能,例如在成员变量声明时给缺省值,以及 final和 override等关键字,对于这些内容感兴趣的可以看相关文档。
在 C++11 前,要传递一个'短小的函数逻辑',要么写独立函数,要么写仿函数(需定义类)—— 这两种方式都很繁琐。而lambda 表达式(匿名函数对象)完美解决了这个问题,支持在函数内部直接定义可调用对象。
lambda 表达式本质上是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式对于使用层面而言没有类型,所以我们一般是用 auto或者模板参数定义的对象去接收 lambda 对象。
lambda 表达式的格式: [capture-list] (parameters)-> return type {function body }
[capture-list]:捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 []来判断接下来的代码是否为 lambda 表达式,捕捉列表能够捕捉上下文中的变量供 lambda 表达式使用,捕捉列表可以传值和传引用捕捉,参数列表无论是否为空都不能省略。(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 ()一起省略。->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{function body} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。下面是一个简单的 lambda表达式:
int main() {
// 一个简单的 lambda 表达式
auto add1 = [](int x, int y) -> int { return x + y; };
cout << add1(1, 2) << endl;
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象自动推导
// 4、函数题不能省略
auto func1 = [] { cout << "hello bit" << endl; return 0; };
func1();
int a = 0, b = 1;
auto swap1 = [](int& x, int& y) { int tmp = x; x = y; y = tmp; };
swap1(a, b);
cout << a << ":" << b << endl;
return 0;
}
lambda 表达式中默认只能用 lambda函数体内和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。一共有三种方法:
[x, y, &z] 表示 x 和 y 是值捕捉,z 是引用捕捉。捕捉列表中的变量名就是想捕捉外层作用域的变量名。=表示隐式值捕捉,在捕捉列表写一个 &表示隐式引用捕捉,可以任意使用外层作用域中的变量,这样我们 lambda表达式中用了那些变量,编译器就会自动捕捉那些变量。[=, &x]表示其他变量隐式值捕捉,而 x 是引用捕捉;[&, x, y]表示其他变量引用捕捉,x 和 y 是值捕捉。当使用混合捕捉时,第一个元素必须是 &或 =,并且 &混合捕捉时,后面的捕捉变量必须是值捕捉,同理 =混合捕捉时,后面的捕捉变量必须是引用捕捉。lambda 表达式如果在函数局部域中,他可以捕捉 lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着 lambda表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下,
lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改。我们可以使用mutable关键字,加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略 (即使参数为空)。
在学习 lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数又要定义一个类,相对会比较麻烦。使用 lambda去定义可调用对象,既简单又方便。
lambda在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等。这里我们举一个排序函数的例子:之前我们使用排序函数时如果要控制具体的排序方式需要传入仿函数,而当我们有了 lambda 表达式后就可以直接定义,不用再去写仿函数,如下面代码所示:
struct Goods {
string _name;// 名字
double _price;// 价格
int _evaluate;// 评价
// ...
Goods(const char* str, double price, int evaluate) :
_name(str), _price(price), _evaluate(evaluate) {}
};
int main() {
vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的比较,相对还是比较麻烦的,那么这里 lambda 就很好用了
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g_evaluate < g_evaluate; });
(v.(), v.(), []( Goods& g1, Goods& g2) { g_evaluate > g_evaluate; });
;
}
lambda的原理和范围 for很像,编译后从汇编指令层的角度看,压根就没有 lambda和范围 for这样的东西。范围 for底层是迭代器,而 lambda底层是仿函数对象,也就是说我们写了一个 lambda表达式以后,编译器会生成一个对应的仿函数的类。
仿函数的类名是编译器按一定规则生成的,保证不同的 lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数 operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda类构造函数的实参,对于隐式捕捉,编译器要看使用了哪些变量,就传哪些变量。
lambda表达式看似'匿名',实则是编译器帮我们隐藏了类定义。
C++ 中的可调用对象(函数指针、仿函数、lambda表达式、成员函数)类型各异,导致在存储或传递时非常不便。C++11 的包装器(std::function和std::bind)解决了这一问题。
std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind 表达式等,存储的可调用对象被称为 std::function的目标。若 std::function不含目标,则称它为空。调用空 std::function的目标会导致抛出 std::bad_function_call异常。
std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
下面是 function的原型,他被定义 <functional>头文件中(官方文档):
template<class T> class function;// undefined
template<class Ret, class... Args> class function<Ret(Args...)>;
函数指针、仿函数、lambda 等可调用对象的类型各不相同,std::function的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。对于 function类模板的参数,有特殊的语法:function<返回值 (参数列表)>,如下面代码所示:
#include <functional>
//普通函数
int f(int a, int b) { return a + b; }
//仿函数
struct Functor {
public:
int operator()(int a, int b) { return a + b; }
};
//成员函数
class Plus {
public:
Plus(int n = 10) :_n(n) {}
static int plusi(int a, int b) { return a + b; }
double plusd(double a, double b) { return (a + b) * _n; }
private:
int _n;
};
int main() {
// 包装各种可调用对象
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(, )> f3 = []( a, b) { a + b; };
cout << (, ) << endl;
cout << (, ) << endl;
cout << (, ) << endl;
function<(, )> f4 = &Plus::plusi;
cout << (, ) << endl;
function<(Plus*, , )> f5 = &Plus::plusd;
Plus pd;
cout << (&pd, , ) << endl;
function<(Plus, , )> f6 = &Plus::plusd;
cout << (pd, , ) << endl;
cout << (pd, , ) << endl;
function<(Plus&&, , )> f7 = &Plus::plusd;
cout << ((pd), , ) << endl;
cout << ((), , ) << endl;
;
}
std::function的典型场景是作为容器的值类型,例如在 逆波兰表达式求值中用 map实现'字符串→可调用对象'的映射:
// 使用 map 映射 string 和 function 的方式实现
// 这种方式的最大优势之一是方便扩展,假设还有其他运算,我们增加 map 中的映射即可
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
// function 作为 map 的映射可调用对象的类型
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int x, int y) { return x + y; }},
{"-", [](int x, int y) { return x - y; }},
{"*", [](int x, int y) { return x * y; }},
{"/", [](int x, int y) { return x / y; }}
};
for (auto& str : tokens) {
if (opFuncMap.count(str)) // 操作符
{
int right = st.top(); st.pop();
int left = st.top(); st.pop();
int ret = opFuncMap[str](left, right);
st.push(ret);
}
else {
st.push(stoi(str));
}
}
return st.top();
}
};
std::bind 是一个函数模板,它也是一个可调用的包装器,可以把他看做一个函数适配器,对接收的可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数顺序。bind也在 <functional>这个头文件中
下面是 bind的定义:
simple(1)
template<class Fn, class... Args> /* unspecified */ bind(Fn&& fn, Args&&... args);
with returntype(2)
template<class Ret, class Fn, class... Args> /* unspecified */ bind(Fn&& fn, Args&&... args);
调用 bind的一般形式:auto newCallable = bind(callable, arg_list);其中 Callable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用 newCallable时,newCallable会调用 callable,并传给它 arg_list中的参数。
arg_list中的参数可能包含形如 _n的名字,其中 n是一个整数,这些参数是占位符,表示 newCallable的参数,它们占据了传递给 newCallable的参数的位置。数值 n表示生成的可调用对象中参数的位置:_1为 newCallable的第一个参数,_2为第二个参数,以此类推。_1/_2/_3…这些占位符被放到 placeholders的一个命名空间中。举一个简单的例子:
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
void Sub(int a, int b, int c) {
cout << a << ' ' << b << ' ' << c << endl;
}
int main() {
auto sub1 = bind(Sub, _2, _1, 3);
sub1(1, 2); // sun1 中的第一个参数永远传给 bind 中的_1,在 sub 中,_1 对应第二个参数,其他同理
// 2 1 3
auto sub2 = bind(Sub, _1, 3, _2);
sub2(1, 2); //1 3 2
return 0;
}
bind可以调整参数顺序,但是并不常用,它最常用的地方在于可以调整参数个数,也就是说可以绑定某些变量,不用每次都传,如下面代码所示:
class Plus {
public:
static int plusi(int a, int b) { return a + b; }
double plusd(double a, double b) { return a + b; }
};
int main() {
// 成员函数对象进行绑死,就不需要每次都传递了
function<double(Plus&&, double, double)> f1 = &Plus::plusd;
Plus pd;
cout << f1(move(pd), 1.1, 1.1) << endl;
cout << f1(Plus(), 1.1, 1.1) << endl;
// bind 一般用于绑死一些固定参数
function<double(double, double)> f2 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f2(1.1, 1.1) << endl;
}
C++11 并非简单的'特性堆砌',而是对 C++ 语言的一次'重塑':
unordered_map、emplace系列)和多线程内存模型,让 C++ 更适应现代软件开发需求。如果你至今仍在使用 C++98 的思维编写代码,不妨从今天开始,尝试用 C++11 的特性重构你的项目 —— 相信我,它会让你的代码更优雅、更高效。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online