C++日新月异的未来代码:C++11(下)
文章目录
接上篇,继续学习C++11的常用新特性
1.lambda表达式
1.1 引入
lambda 表达式是一种匿名函数对象,允许在代码中直接定义和使用小型的函数,无需额外定义函数或函数对象类,这么讲,感觉还是太理论了,下面将通过特定场景介绍其使用:
structGoods{ string _name;// 名字double _price;// 价格int _evaluate;// 评价Goods(constchar* str,double price,int evaluate):_name(str),_price(price),_evaluate(evaluate){}};structComparePriceLess{booloperator()(const Goods& gl,const Goods& gr){return gl._price < gr._price;}};structComparePriceGreater{booloperator()(const Goods& gl,const Goods& gr){return gl._price > gr._price;}};intmain(){ vector<Goods> v ={{"苹果",2.1,5},{"香蕉",3,4},{"橙子",2.2,3},{"菠萝",1.5,4}};sort(v.begin(), v.end(),ComparePriceLess());sort(v.begin(), v.end(),ComparePriceGreater());}日常生活中,一件商品包含多个特性,若想针对某个特性进行排序,那么就需要使用算法库里的 sort,设置自定义类型的比较方式,那么仿函数就是个很好的方式
随着 C++ 语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个 algorithm 算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在 C++11 语法中出现了 lambda 表达式
1.2 语法
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }lambda表达式各部分说明:
[capture-list]: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用(parameters):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)->returntype:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导{statement}:函数体,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
🔥值得注意的是: 在 lambda 函数定义中,参数列表和返回值类型都是可选忽略部分,而捕捉列表和函数体可以为空。因此 C++11 中最简单的 lambda 函数为:[]{}; 该lambda 函数不能做任何事情
1.3 使用
intmain(){ vector<Goods> v ={{"苹果",2.1,5},{"香蕉",3,4},{"橙子",2.2,3},{"菠萝",1.5,4}};sort(v.begin(), v.end(),[](const Goods& g1,const Goods& g2)->bool{return g1._price < g2._price;});sort(v.begin(), v.end(),[](const Goods& g1,const Goods& g2)->bool{return g1._price > g2._price;});sort(v.begin(), v.end(),[](const Goods& g1,const Goods& g2)->bool{return g1._evaluate < g2._evaluate;});sort(v.begin(), v.end(),[](const Goods& g1,const Goods& g2)->bool{return g1._evaluate > g2._evaluate;});return0;}因此,lambda 表达式可以这样套用在 sort 里,比仿函数确实方便且可观性更高了,可以看出 lambda 表达式实际是一个匿名函数(无名函数),该函数无法直接调用,如果想要直接调用,可借助 auto 将其赋值给一个变量
auto ret= [ ](const Goods& g1,const Goods& g2) {return g1._evaluate<g2._evaluate; }
对于捕捉列表 [],平常一般使用的不多,但是某些情况还是要使用的,需要了解其用法
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用:
[x]:表示值传递方式捕捉变量x[=]:表示值传递方式捕获所有父作用域中的变量(包括this)[&x]:表示引用传递捕捉变量x[&]:表示引用传递捕捉所有父作用域中的变量(包括this)[this]:表示值传递方式捕捉当前的this指针
🔥值得注意的是:
- 父作用域指包含
lambda函数的语句块 lambda默认以值传递的方式进行,传值捕捉的变量是不可修改的
intmain(){int x =10;auto func =[x]()mutable{ x =20; cout << x << std::endl;};func(); cout << x << endl;return0;}使用 mutable 关键字就可以修改了,但是这种修改只是对 lambda 内部的副本进行修改,不会影响到原始的变量。在 main 函数中再次输出 x 时,其值仍为 10
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:
[=, &a, &b],以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this],值传递方式捕捉变量a和this,引用方式捕捉其他变量 - 捕捉列表不允许变量重复传递,否则就会导致编译错误
- 在块作用域以外的
lambda函数捕捉列表必须为空,在全局作用域中,并没有局部变量可供lambda函数捕获 - 在块作用域中的
lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错 lambda表达式之间不能相互赋值,即使看起来类型相同,但是可以拷贝构造(每个lambda表达式都有其独特的、未命名的类型。即使两个lambda表达式的参数列表和返回类型相同,它们的类型也是不同的)
1.4 本质

转到反汇编可以发现,其实 lambda 的本质就是被包装的仿函数,编译器会自动生成一个类,在该类中重载了 operator()
2.类的新增语法
2.1 移动构造、移动赋值运算符
C++11 新增了两个:移动构造函数和移动赋值运算符重载,在上一篇有进行详细的说明
传送门:C++日新月异的未来代码:C++11(上)
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
2.2 default
classPerson{public:Person(constchar* name ="",int age =0):_name(name),_age(age){}Person(const Person& p):_name(p._name),_age(p._age){}Person(Person && p)=default;private: bit::string _name;int _age;};intmain(){ Person s1; Person s2 = s1; Person s3 = std::move(s1);return0;}default 是强制生成默认函数的关键字,我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成
2.3 delete
classPerson{public:Person(constchar* name ="",int age =0):_name(name),_age(age){}Person(const Person& p)=delete;private: bit::string _name;int _age;};intmain(){ Person s1; Person s2 = s1; Person s3 = std::move(s1);return0;}delete 是禁止生成默认函数的关键字,当类显式删除了拷贝构造函数时,编译器不会自动生成移动构造函数(即使没有显式删除移动构造函数),代码中没有显式定义移动构造函数,且隐式移动构造函数被禁用,因此无法完成移动初始化
🔥值得注意的是:
移动构造函数的核心目的是高效转移资源所有权(如动态内存、文件句柄等),而拷贝构造函数的目的是创建资源的独立副本。如果一个类禁用了拷贝构造函数,通常意味着:
- 资源不可复制: 例如独占式资源,拷贝会导致资源管理混乱
- 防止意外拷贝: 开发者希望禁止对象的复制操作,强制使用移动语义
此时,如果编译器仍然自动生成移动构造函数,可能会破坏这种设计意图
3.可变参数模板
3.1 概念

其实可变模板参数早在C语言就已经有了,后面三个点点点就是可变模板参数,比如: printf("%d,%d,%d", x, y, z),后面的参数个数是可以自己控制有多少个的,这就是一种可变模板参数
template<class...Args>voidShowList(Args... args){}intmain(){ShowList();ShowList(1);ShowList(1,2.2);ShowList(1,2,"xxxxx");return0;}回到实际定义,Args 是一个模板参数包,args 是一个函数形参参数包,声明一个参数包Args... args,这个参数包中可以包含 0 到任意个模板参数
3.2 获取个数
template<class...Args>voidShowList(Args... args){ cout <<sizeof...(args)<< endl;}intmain(){ShowList();ShowList(1);ShowList(1,2.2);ShowList(1,2,"xxxxx");return0;}这个用法也是很奇葩。。。
3.3 展开参数包
不知道当初设计怎么想的,这里想要 for 循环遍历展开是不可行的,编译器不支持,所以这里的展开方法做了解即可
3.3.1 递归函数
// 递归终止函数template<classT>voidShowList(const T& t){ cout << t << endl;}// 展开函数template<classT,class...Args>voidShowList(T value, Args... args){ cout << value <<" ";ShowList(args...);}intmain(){ShowList(1);ShowList(1,'A');ShowList(1,'A',string("sort"));return0;}模式匹配: 展开函数 ShowList(T value, Args... args) 匹配 至少一个参数 的情况,每次取出第一个参数 value,剩余参数构成新的参数包 args... ,终止函数 ShowList(const T& t) 匹配仅有一个参数 的情况,结束递归
参数包展开:args... 在递归调用时会被解包,每次减少一个参数,直到参数包为空,
关键语句 ShowList(args...) 会触发模板的递归实例化,直到匹配终止函数
输出顺序: 先打印当前参数 value,再递归处理剩余参数,确保参数按传入顺序输出
3.3.2 逗号表达式
template<classT>voidPrintArg(T t){ cout << t <<" ";}//展开函数template<class...Args>voidShowList(Args... args){int arr[]={(PrintArg(args),0)...}; cout << endl;}intmain(){ShowList(1);ShowList(1,'A');ShowList(1,'A',string("sort"));return0;}(PrintArg(args), 0)... 会将参数包 args... 展开为多个表达式,打印对应的值,然后返回 0(用于填充数组)
// 原始代码int arr[]={(PrintArg(args),0)...};// 展开后等价于int arr[]={(PrintArg(1),0),(PrintArg('A'),0),(PrintArg("sort"),0)};PrintArg 的返回值是 void,无法初始化 int 数组,即使 PrintArg 返回参数类型(如 T),参数包可能包含不同类型(如 int, char),仍会导致类型不匹配
每个元素必须是 int 类型,因此需要用 0 作为统一的返回值,保证初始化的数组元素都为相同类型
3.4 emplace系列的接口
intmain(){ list< pair<int,char>> mylist; mylist.emplace_back(10,'a'); mylist.emplace_back(20,'b'); mylist.emplace_back(make_pair(30,'c')); mylist.push_back(make_pair(40,'d')); mylist.push_back({50,'e'});for(auto e : mylist) cout << e.first <<":"<< e.second << endl;return0;}emplace_back 的作用和 push_back 相同,但是 mylist.emplace_back(20, 'b') 这种格式的写法更方便一些
其实我们会发现其实差别也不大,emplace_back 是直接构造了,push_back 是先构造,再移动构造,移动构造的消耗很小,其实没啥影响
3.5 可变参数模板的实际应用
classDate{public:Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){ cout <<"Date构造"<< endl;}Date(const Date& d):_year(d._year),_month(d._month),_day(d._day){ cout <<"Date拷贝构造"<< endl;}private:int _year;int _month;int _day;};template<class...Args> Date*Create(Args... args){ Date* ret =newDate(args...);return ret;}intmain(){ list<Date> lt; Date d(2023,9,27);// 只能传日期类对象 lt.push_back(d);// 既能传日期类对象// 又能传日期类对象的参数包// 参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象 lt.emplace_back(d); lt.emplace_back(2023,9,27);return0;}push_back 只能传日期类对象,emplace_back 既能传日期类对象,又能传日期类对象的参数包。参数包,一路往下传,直接去构造或者拷贝构造节点中日期类对象
4.包装器
4.1 function
template<classF,classT> T useF(F f, T x){staticint count =0; cout <<"count:"<<++count << endl; cout <<"count:"<<&count << endl;returnf(x);}doublef(double i){return i /2;}structFunctor{doubleoperator()(double d){return d /3;}};intmain(){// 函数名 cout <<useF(f,11.11)<< endl;// 函数对象 cout <<useF(Functor(),11.11)<< endl;// lamber表达式 cout <<useF([](double d)->double{return d /4;},11.11)<< endl;return0;}我们知道函数指针,仿函数,lambda表达式,这三种都是函数对象的创建方式,同时调用这三个方式实例化模板,useF函数模板实例化了三份,明明都是相同的内容,实在是没有必要,会导致模板的效率低下
那么这种时候就需要使用头文件 <functional> 中的 function,function 包装器也叫作适配器。C++中的 function 本质是一个类模板,也是一个包装器
// 类模板原型如下template<classT> function;// undefinedtemplate<classRet,class... Args>classfunction<Ret(Args...)>;模板参数说明:
Ret: 被调用函数的返回类型Args…:被调用函数的形参
下面直接修改以上代码,来展示 function 的使用效果:
template<classF,classT> T useF(F f, T x){staticint count =0; cout <<"count:"<<++count << endl; cout <<"count:"<<&count << endl;returnf(x);}doublef(double i){return i /2;}structFunctor{doubleoperator()(double d){return d /3;}};intmain(){// 函数名 function<double(double)> func1 = f; cout <<useF(func1,11.11)<< endl;// 函数对象 function<double(double)> func2 =Functor(); cout <<useF(func2,11.11)<< endl;// lamber表达式 function<double(double)> func3 =[](double d)->double{return d /4;}; cout <<useF(func3,11.11)<< endl;return0;}三种可调用对象被统一为同一类型:包装类,模板只实例化一次,静态变量共享(即这个 count 只有一份),
4.2 bind
bind 函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
bind 可以理解为一个接收包装类的适配器,上面的例子都是直接将函数给到包装类,那么 bind 就是将特定的函数和参数绑定到包装类,通过例子解析会更容易理解:
intPlus(int a,int b){return a + b;}classSub{public:intsub(int a,int b){return a - b;}};intmain(){//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定 std::function<int(int,int)> func1 = std::bind(Plus, placeholders::_1, placeholders::_2);//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);//func2的类型为 function<void(int, int, int)> 与func1类型一样//表示绑定函数 plus 的第一,二为: 1, 2auto func2 = std::bind(Plus,1,2); cout <<func1(1,2)<< endl; cout <<func2()<< endl; Sub s;// 绑定成员函数 std::function<int(int,int)> func3 = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2);// 参数调换顺序 std::function<int(int,int)> func4 = std::bind(&Sub::sub, s, placeholders::_2, placeholders::_1); cout <<func3(1,2)<< endl; cout <<func4(1,2)<< endl;return0;}bind 的第一个参数传的是函数,后面的是一系列要传的参数,_1 为第一个参数,_2 为第二个参数,以此类推,参数既可以是待定的,也可以是具体的值,placeholders 属于 std 命名空间,若展开了就不用写

🔥值得注意的是:
- 若函数是非静态成员函数,必须在
Sub::sub前加上&,因为非静态成员函数依赖对象,必须显式调用其地址,普通函数指针直接指向代码地址,而成员函数指针需要同时包含类的类型信息和函数地址,因此还需要将对象s传过去 - 若函数是静态成员函数,和普通函数一样都是全局函数,就不需要加
&和传对象
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!
