C++11标准 上 (万字解析)
目录
一、C++11的初始化
std::initializer_list
二、左值引用和右值引用
1、左值和右值的定义
左值:
右值:
2、左值引用和右值引用
move:
3、左右值引用的应用场景
1.延长生命周期
2.左值引用的缺陷
3.移动构造和移动赋值
4、类型分类
5、引用折叠
6、完美转发
三、可变参数模板
1、基本语法与定理
2、包扩展
3、emplace系列接口
emplace的推荐使用场景:
推荐insert、push_back等传统接口的场景:
四、新的类功能
1、默认的移动构造和移动赋值
2、defult和delete
五、lambda
1、lambda表达语法
2、捕捉列表
3、lambda的应用
六、包装器
1、function
2、bind
核心作用:
基本语法:
作用场景:
bind和lambda的对比:
一、C++11的初始化
C++11标准中试图将一切对象都能使用 { } 初始化(列表初始化),使用 { } 初始化时可以省略符号“=”。
int a = { 1 };
int b{ 2 };
//自定义的Date类
//x1构造加拷贝构造直接优化成构造
Date x1 = { 2025,10,17 };
//x2直接构造
Date x2{ 2025,10,17 };
const Date& x3 = {2025,10,17};
x1的初始化在语法上是先将 { }中的元素用于初始化临时对象,再将临时对象拷贝构造给x1。实际上部分编译器会将该过程优化为直接构造。
x2直接调用构造函数构造。
x3是const Date&类型,在临时对象创建的过程中x3会绑定在临时对象身上。根据 C++ 标准,被绑定的临时对象的生命周期会被强制延长至与 x3 一致(即x3所在作用域结束时,临时对象才销毁)。
std::initializer_list
C++11中提出了initializer_list这个类,该类的本质就是在底层开一个数组并将数据拷贝过去。并且initializer_list类中还包含两个指针指向数组开始和结束的位置,并且其也支持迭代器遍历。
出于其能够有效的方便用多个值初始化对象,在C++11后绝大多数的容器中都新添了initializer_list构造初始化。
vector<int> x4{ 1,2,3,4,5,6,7 };//用到了initializer_list初始化
样例解释:
1、在上面的x4中,std::initializer_list<int>先创建一个临时对象,其中存储着{1,2,3,4,5,6,7}这个临时数组的数组指针。
2、然后vector的initializer_list构造函数被调用,创建vector<int>对象x4。initializer_list构造函数会遍历临时对象中的数据并逐个复制到x4中。
3、初始化完成后,临时的initializer_list对象和其临时数组被销毁。
二、左值引用和右值引用
1、左值和右值的定义
左值:
左值是⼀个表示数据的表达式(如变量名或解引用的指针),⼀般是有持久存储位置,我们可以获取它的地址,可作为赋值运算符的左操作数(除非被const修饰)
左值可以出现赋值符号的左边,也可以出现在赋值符号右边。
右值:
右值也是⼀个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象 (没有持久存储位置)等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
区分左右值的关键就看其能否被取地址!
在现代C++中,左值lvalue通常会解释为loactor value也就是意为存储在内存中、有明确存储地址可以取地址的对象;右值rvalue通常会被解释为read value也就是那些可以提供数据值,但是不可以寻址的对象。
2、左值引用和右值引用
先看这段代码:
int& a = 10;
很明显上段代码会导致编译报错,在C++11之前我们对于引用常量需要进行const修饰,在C++11之后我们还何以使用右值引用来解决这个问题。
可以先得到两个规则:
1、type& x=n是左值引用;type&& y=m是右值引用。左值引用就是给左值取别名,右值引用就是给右值取别名。
2、左值引用不能直接引用右值,但可以通过const修饰引用右值。
既然const左值引用能够引用右值,那么右值引用能引用左值吗?当然可以。
3、右值引用不能直接引用左值,但是右值引用可以move(左值)。
move:
move是一个函数模板,本质上还是强制类型转化(当然还包含一些折叠方面的知识)。
在上面的例子(int&& c=move(b);)中,左值b被move"转化"为右值。值得注意的是:
1.被强制转化为右值b其左值属性并没有因此改为右值
解释:move(b)只是生成一个右值引用类型的表达式,b只是被临时转化,但b作为左值的本质并没有改变。并且b依旧能取到地址(&b)。
2.变量c的类型虽然为int&&,但其作为表达式(能取到地址)依旧是左值而不是右值。
解释:c作为右值引用类型能引用右值,但c依旧是有名字的变量,依旧能被取到地址。
3.表达式move(b)的结果是右值,它的作用是 “告诉编译器:可以安全地移动b的资源”。它是一个指向原对象b的右值引用表达式
另外说个有趣的现象:左值引用和右值引用虽然是不同类型对象的引用且不需要开空间,但在底层汇编的角度上来看这两个都是通过指针来实现的,两者没有多大的区别。
除了move以外,我们可以直接强制将左值转化为右值进行右值引用:
3、左右值引用的应用场景
1.延长生命周期
在上面讲解x3对象时其实就是const左值引用延长临时对象(右值)生命周期的一个场景。
除了const左值引用能延长右值生命周期,右值引用(type&& x)也能达到这个效果。
另外,const左值引用对象不能被修改而右值引用对象可以发生修改。
2.左值引用的缺陷
我们在设计某一功能函数的时候有可能会出现返回值为临时对象并且传参只能进行传值传参的情况,在C++11之前我们只能被动进行传值返回触发多次拷贝从而导致效率大大降低。
在C++11扩展右值这一概念后我们便有了解决办法:
3.移动构造和移动赋值
class MyString
{
private:char* data;size_t length;
public:void swap(MyString& s){std::swap(data, s.data);std::swap(length, s.length);}// 移动构造函数:转移资源(开销小)MyString(MyString&& other) //noexcept{ swap(other);std::cout << "移动构造" << std::endl;}//移动赋值MyString& operator=(MyString&& other){swap(other);std::cout << "移动赋值" << std::endl;return *this;}
};
MyString类中的移动构造MyString(MyString&& other)能够避免函数返回值为临时对象并且传参只能进行传值传参时多次拷贝的问题。因为临时对象最终还是要销毁,移动构造干脆将把临时对象的“货物”强到自己手中。这样当临时对象销毁时就不会影响到自己手里被抢来的“新货物”,并且还减小拷贝提高效率。
移动赋值的实现逻辑与作用与移动拷贝相似,其目的都是抢夺右值资源减少拷贝。
A::MyString c = A::MyString("cccc");原本是构造+移动构造(A::MyString("cccc")会产生临时对象),但被编译器优化为直接构造。
右值引用在进行深拷贝等特殊场景的时候会明显比左值引用更高效,但我们别忘了编译器本身的优化。有时候就像上面c对象编译器直接将构造+移动构造优化为直接构造会带来更大的效率提升。
在C++11后,容器中部分接口新添右值引用版本:
其优势详见其下几个知识点。
4、类型分类
C++11右值概念扩展后也对类型进行重新分类,右值被划分纯右值(pure value,简称prvalue)和将亡值 (expiring value,简称xvalue)。
纯右值
纯右值是指那些字面值常量或求值结果相当于字面值或是⼀个不具名的临时对象。
如字面常量10、布尔true、nullptr;s1+s2(std::string类型)这类传值返回函数调用的不具名临时对象;表达式求值(非对象标识)如整形a,a+b,a++等。
将亡值
将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,其核心特征是:“有标识(可寻址)但即将被销毁,其资源可以安全地被移动”。如move(x)的右值结果
亡值是连接 “左值” 和 “移动语义” 的关键 —— 它让原本是左值的对象(有名字、可寻址)在 “即将废弃” 时,能被当作右值处理,从而触发移动构造 / 移动赋值,实现资源的高效转移。
泛左值
泛左值(generalized value,简称glvalue)包含左值和将亡值。
5、引用折叠
C++中不能直接定义引用的引用如 int& && x= y; ,这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用
template<class T>//模板可以实例化为左/右值引用类型
void f1(T& x)
{}template<class T>
void f2(T&& x)
{}typedef int& left;
typedef int&& right;
C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
上面的函数模板f2在折叠规则下传入左值就识别为左值,传入右值就识别为右值。这样的函数模板我们成为万能引用。
6、完美转发
Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化 以后是右值引用的Function函数。
变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性, 就需要使用完美转发(万能引用+forward)实现。
在 C++ 中,完美转发(Perfect Forwarding) 是指在函数模板中,将参数原封不动地转发给其他函数 —— 既保留参数的值类别(左值 / 右值),也保留其const/volatile 等属性,从而确保目标函数能根据参数的原始类型正确匹配重载(如区分左值引用参数和右值引用参数)。
// 目标函数:区分左值和右值
void process(int& x) { // 左值版本:处理需要保留的对象std::cout << "处理左值:值为" << x << ",即将修改它\n";x *= 2; // 修改左值(原对象会被影响)
}void process(int&& x) { // 右值版本:处理可移动的临时对象std::cout << "处理右值:值为" << x << ",这是临时对象\n";// 右值是临时的,修改它不影响其他地方
}// 中间转发函数:不用完美转发(错误示例)
template <typename T>
void wrap_process_bad(T&& param) {std::cout << "[错误转发] 开始处理...\n";// 直接传递param:此时param是有名字的左值(即使它绑定右值)process(param);
}// 中间转发函数:使用完美转发(正确示例)
template <typename T>
void wrap_process_good(T&& param) {std::cout << "[完美转发] 开始处理...\n";// 用std::forward<T>还原param的原始值类别process(std::forward<T>(param));
}int main() {int a = 10; // 左值std::cout << "=== 测试左值转发 ===\n";wrap_process_bad(a); // 错误转发:正确调用左值版本(因为a是左值)wrap_process_good(a); // 完美转发:正确调用左值版本std::cout << "左值修改后:" << a << "\n\n"; // 验证a被修改std::cout << "=== 测试右值转发 ===\n";wrap_process_bad(20); // 错误转发:右值被当作左值,调用左值版本(错误)wrap_process_good(20); // 完美转发:正确调用右值版本(正确)return 0;
}
上面wrap_process_bad中虽然是用来万能引用但是变量表达式的属性是左值,导致如果是右值引用参数传递过去时性质被误判为左值而被错误修改!
wrap_process_good使用std::forward<T>(param)根据模板参数T的类型,将参数param还原为原始的值类别(右值)。
三、可变参数模板
1、基本语法与定理
C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表。
函数参数包可以用左值引用或右值引用表示,跟前面普通模板⼀样,每个参数实例化时遵循引用折叠规则。
参数包本身不能直接使用,必须通过 ...
运算符 “展开” 为独立的参数序列。展开后,包中的每个元素会被单独处理。
可以使用sizeof...运算符去计算参数包中参数的个数。
2、包扩展
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它。
当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
包的扩展通常是通过递归实现的。每次递归的时候会取旧包中的第一个元素参与具体逻辑计算,剩下的元素作为新包继续递归执行操作。但当包中的元素用完(为空)后递归版本的函数(含参函数)无法处理这种情况,需要在外面再额外创建一个无参函数来处理递归终止情况。
3、emplace系列接口
C++11以后STL容器新增了empalce系列的接口,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。
emplace的推荐使用场景:
1.当需要通过构造参数创建新元素(尤其是复杂对象)时,优先用emplace。
list<string> lt;
// 直接把构造string参数包往下传,emplace接口直接⽤string参数包构造string
// 这⾥达到的效果是push_back做不到的
//push_back的参数需是string类型
lt.emplace_back("111111111111");
2.对于map
、unordered_map
等,emplace
可以直接传递键和值的构造参数,避免手动构造pair
对象(如std::make_pair
的开销)
推荐insert、push_back等传统接口的场景:
1.如果已经有一个构造好的对象(而非构造参数),此时push_back(std::move(obj))
通常比emplace
更高效且直观。
2.对于int
、double
等内置类型,或构造 / 拷贝成本极低的轻量对象(如仅包含几个成员的小结构体),emplace
和push_back
的性能差异可忽略。此时push_back
的可读性更优(更符合直觉)
3.emplace
会直接匹配元素的构造函数,可能触发意外的隐式转换(尤其是单参数构造函数),而push_back
需要先构造元素类型的对象,隐式转换的场景更受限,安全性更高。
四、新的类功能
1、默认的移动构造和移动赋值
C++11在原来类的6个默认成员函数(构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重 载/const取地址重载)的基础上还新加了默认的移动构造和移动赋值。
如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成⼀个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意⼀个,那么编译器会自动生成⼀个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2、defult和delete
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为⼀些原因 这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
C++11中在该函数声明加上=delete表示该语法指示编译器不生成对应函数的默认版本,故称=delete修饰的函数为删除函数。
五、lambda
1、lambda表达语法
lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使用层而言没有类型,所以我们⼀般是用auto或者模板参数定义的对象去接收lambda 对象。
lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
capture-list] :捕捉列表,该列表总是出现在 lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。
(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略。
->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
2、捕捉列表
int main()
{int a = 0, b = 1, c = 2, d = 3;auto fun1 = [a, &b]()->void{ //只捕获a,b;c,d并没有被捕获也无法使用//a++; a为值捕捉不能被修改b++;cout << a + b << endl << "***************" << endl;};fun1();auto fun2 = [=]//返回类型被省略,同理由于不需要传参()也可以省略{//= 为隐式值捕获,被捕获对象不能被修改//⽤了哪些变量就捕捉哪些变量 cout << a + b+c+d << endl << "***************" << endl;//返回值推断为void类型}; fun2();auto fun3 = [&]{ //隐式引用捕获,被捕获对象可以被修改//⽤了哪些变量就捕捉哪些变量 a++; b++;c++;cout << a + b+c+d << endl << "***************" << endl;};fun3();auto fun4 = [&,c]{//混合捕获,除了c明确为值捕获以外其它用到的变量为引用捕获a++;b++;//c++;cout << a + b+c+d << endl << "***************" << endl;}; fun4();auto fun5 = [=, &a]{//混合捕获,除了a明确为引用捕获以外其它用到的变量为值捕获a++;//b++;//c++;cout << a + b + c + d << endl << "***************" << endl;}; fun5();return 0;
}
3、lambda的应用
之前我们用过仿函数来控制一个排序算法的升/降序,lambda同样也可以做到这一点。并且使用lambda能降低复杂度并且时排序比较方式更加直观。
struct Person
{std::string name;int age;
};int main()
{std::vector<Person> people = {{"Alice", 25}, {"Bob", 20}, {"Charlie", 30}};// 按年龄升序:lambda直接访问age成员std::sort(people.begin(), people.end(),[](const Person& p1, const Person& p2) { return p1.age < p2.age; });// 按年龄降序:仅修改比较符号std::sort(people.begin(), people.end(),[](const Person& p1, const Person& p2) { return p1.age > p2.age; });return 0;
}
在C++17之后lambda被补充增强,其被广泛的应用于元模板编程。
六、包装器
1、function
C++ 的std::function是通用函数包装器,定义在<functional>头文件中,核心作用是 “类型擦除”—— 能存储、拷贝、调用各种不同类型的可调用对象(如普通函数、lambda、仿函数、类成员函数等),并将它们统一为相同的类型,方便统一管理和传递。
std::function的模板参数格式为:std::function<返回值类型(参数类型1,参数类型2……)>
函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function的优势就是统⼀类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型
int F1(int x, int y)
{return x + y;
}struct F2
{int operator()(int x, int y){return x + y;}
};int main()
{std::function<int(int, int)> f1 = F1;std::function<int(int, int)> f2 = [](int x, int y)->int {return x + y; };std::function<int(int, int)> f3 = F2();std::cout << f1(1, 2) << std::endl;std::cout << f2(3, 4) << std::endl;std::cout << f3(5, 6) << std::endl;return 0;
}
struct T
{static int F3(int x, int y){return x + y;}int F4(int x, int y){return x * y;}
};
//包装static静态成员函数
//对于静态成员函数需要在类名前面取地址来获取函数指针
std::function<int(int, int)> f4 = &T::F3;
std::cout << f4(7, 8) << std::endl;//包装普通成员函数
//普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
std::function<int(T*,int, int)> f5 = &T::F4;
T text1;
std::cout << f5(&text1,9, 10) << std::endl;std::function<int(T, int, int)> f6 = &T::F4;
T text2;
std::cout << f6(text2, 10, 10) << std::endl;
例题:
//都是逆波兰表达式求解,但没按照上面的题目来
#include<iostream>
#include<stack>
#include<utility>
#include<string>
#include<vector>
#include<map>
#include<functional>using namespace std;vector<string> S{ "12", "1", "+", "221", "362", "/", "-", "100", "+", "200", "+" };int main()
{stack<int> K;//map中存pair键链值map<string, function<int(int x, int y)>>Fun={{"+", [](int x, int y) {return x + y; }},{ "-", [](int x, int y) {return y - x; }},{ "*", [](int x, int y) {return x * y; } },{ "/", [](int x, int y) {return y /x ; }}};for (auto& e : S){if (Fun.count(e))//判断是不是符号{int r1 = K.top();K.pop();int r2 = K.top();K.pop();int ret = Fun[e](r1, r2);K.push(ret);}else{K.push(stoi(e));}}cout << K.top();return 0;
}
int ret = Fun[e](r1, r2)运行原理:
假设e是“+”,r1=3
,r2=5。
2、bind
C++ 中的bind是定义在<functional>头文件中的工具,核心作用是绑定函数(或可调用对象)的部分 / 全部参数,生成一个新的可调用对象。它能灵活调整参数的传递方式(固定参数、调整顺序、绑定成员函数等),本质是 “参数适配器”。
核心作用:
基本语法:
作用场景:
1.固定函数参数
using namespace std;
using namespace std::placeholders; // 引入占位符// 原函数:计算 a + b
int add(int a, int b) {return a + b;
}
int main()
{// 绑定add的第一个参数为5,生成新函数:add5(b) = 5 + bauto add5 = bind(add, 5, _1); // _1表示后续调用时传入的第1个参数(即b)// 调用新函数:传入b=3,实际计算5+3cout << add5(3) << endl; // 输出8return 0;
}
2.调整参数传递顺序
using namespace std;
using namespace std::placeholders;// 原函数:计算 a - b
int sub(int a, int b)
{return a - b;
}int main()
{// 原函数sub(a,b)是“a减b”,现在调整为“b减a”(交换参数顺序)// 新函数sub_rev(x,y) = sub(y, x) , 即 y - xauto sub_rev = bind(sub, _2, _1); // 调用sub_rev(3,5) → 实际调用sub(5,3) → 5-3=2cout << sub_rev(3, 5) << endl; // 输出2return 0;
}
3.绑定类的非静态成员函数
using namespace std;
using namespace std::placeholders;class Calculator
{
public:// 非静态成员函数:计算 a * b(隐含this指针)int multiply(int a, int b) {return a * b;}
};int main()
{Calculator calc; // 创建对象实例// 绑定成员函数:第一个参数是“成员函数地址”,第二个参数是“对象指针/引用”// 新函数mul_obj(x,y) = calc.multiply(x,y)auto mul_obj = bind(&Calculator::multiply, &calc, _1, _2); // 调用新函数:传入2和3 → calc.multiply(2,3)=6cout << mul_obj(2, 3) << endl; // 输出6// 也可以固定一个参数:比如固定a=4,新函数mul4(y)=calc.multiply(4,y)auto mul4 = bind(&Calculator::multiply, &calc, 4, _1);cout << mul4(5) << endl; // 输出20return 0;
}
4.绑定 lambda 表达式
using namespace std;
using namespace std::placeholders;int main()
{auto lambda = [](int x, int y, int z) {return x * y + z;};// 绑定lambda的x=2、z=5,生成新函数:new_lambda(y) = 2*y +5auto new_lambda = bind(lambda, 2, _1, 5);cout << new_lambda(3) << endl; // 2*3+5=11,输出11return 0;
}
bind和lambda的对比:
维度 | std::bind | lambda 表达式 |
---|---|---|
核心优势 | 复用已存在的函数(无需重新定义逻辑),灵活绑定参数 | 临时定义逻辑,语法更直观、简洁,支持捕获外部变量 |
语法复杂度 | 依赖占位符,参数顺序调整时易出错(如_1 和_2 混淆) | 逻辑与参数传递直接关联,可读性更高 |
适用场景 | 1. 复用已有函数(如库函数)2. 批量绑定参数生成新函数 | 1. 临时逻辑(仅用一次)2. 需要捕获外部变量(如局部变量) |