ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++11 :新的类功能,lambda,包装器

C++11 :新的类功能,lambda,包装器 目录一.新的类功能1.1默认的移动构造和移动赋值1.2成员变量给缺省值1.3 defult和delete1.4 final和override二.lambda2.1lambda表达式语法及应用2.1.1lambda的表达是语法2.2.2lambda的应用2.2捕捉列表三.包装器3.1function包装器3.2bind包装器一.新的类功能1.1默认的移动构造和移动赋值原来 C 类中有 6 个默认成员函数构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 /const 取地址重载最后重要的是前 4 个后两个用处不大默认成员函数就是我们不写编译器会生成一个默认的。C11 新增了两个默认成员函数移动构造函数和移动赋值运算符重载。如果你没有自己实现移动构造函数且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数对于内置类型成员会执行逐成员按字节拷贝自定义类型成员则需要看这个成员是否实现移动构造如果实现了就调用移动构造没有实现就调用拷贝构造。如果你没有自己实现移动赋值重载函数且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数对于内置类型成员会执行逐成员按字节拷贝自定义类型成员则需要看这个成员是否实现移动赋值如果实现了就调用移动赋值没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值编译器不会自动提供拷贝构造和拷贝赋值。注这里用自定义的string可以更加方便我们观察出调用哪一个函数类型string代码在上一篇文章中已经写过不知道的可以看上一个文章默认成员函数编译器何时自动生成关键抑制条件只要满足不再自动生成默认构造函数用户没有手写任何构造函数自己实现任意构造拷贝构造、移动构造、普通构造等析构函数用户没有手写析构函数显式定义析构函数拷贝构造函数用户没有手写拷贝构造1. 显式定义拷贝构造2. 显式定义移动构造 / 移动赋值C11 规则拷贝赋值运算符用户没有手写拷贝赋值1. 显式定义拷贝赋值2. 显式定义移动构造 / 移动赋值C11 规则移动构造函数C11同时满足全部 3 条① 无自定义移动构造② 无自定义析构③ 无自定义拷贝构造④ 无自定义拷贝赋值只要定义下面任意一个析构 / 拷贝构造 / 拷贝赋值 / 移动构造移动赋值运算符C11同时满足全部 3 条① 无自定义移动赋值② 无自定义析构③ 无自定义拷贝构造④ 无自定义拷贝赋值只要定义下面任意一个析构 / 拷贝构造 / 拷贝赋值 / 移动赋值现代 C 推荐能使用default/delete显式声明避免编译器隐式行为带来坑。1.2成员变量给缺省值1、基础语法C11 新增只能给非静态成员变量写缺省值两种写法struct Node { int _a 10; // 等号形式 double _b{3.14}; // 列表初始化推荐防止隐式转换 int* _ptr nullptr; static int _c 1; // ❌ 错误静态成员不能这样写静态只能类外初始化 };2、核心优先级规则【面试高频】构造函数初始化列表 类内缺省值逻辑如果构造函数初始化列表显式列出该成员→ 使用列表的值类内缺省值直接失效如果初始化列表没有写这个成员→ 自动使用类内缺省值初始化构造函数体内赋值不参与初始化只是赋值初始化早已完成。示例演示struct Node { int _x 100; Node() : _x(1) {} // 初始化列表覆盖 → _x 1 Node(int a) {} // 列表没写_x → 使用缺省值 _x 100 };3、初始化顺序重中之重成员初始化顺序只按照类中从上到下声明顺序和初始化列表书写顺序、类内缺省值书写顺序无关struct Test { int a 1; int b a; // 合法a先初始化 };4、和八大默认成员函数的关系类内缺省值 ≠ 默认构造函数类内缺省值只是 “后备初始化方案”不会强制编译器生成默认构造函数。 只要你手写任意构造函数编译器不再合成默认构造就算写了成员缺省值也没用。概念要求调用方式默认构造函数调用时允许不传参数无参 / 全部参数带缺省A obj;合法普通有参构造函数必须提供实参无法无参调用A obj;报错A obj(10);合法编译器合成的默认构造 / 默认移动构造 / 默认拷贝构造在逐个初始化成员时 如果成员没有在隐式初始化列表中指定就采用类内缺省值初始化。⚠️ 易错区分struct listNode { T _data; Node* _next nullptr; Node* _prev nullptr; };当你创建listNode对象 只要构造函数没有初始化_next、_prev自动初始化为nullptr不用在每一个构造函数初始化列表重复写_next(nullptr),_prev(nullptr) 这就是工程上最常用的好处减少重复代码。5、常见限制与坑不能初始化引用成员、const 成员引用 /const T 成员必须在构造初始化列表显式初始化类内缺省值无法满足。struct A { const int x 5; // ❌ 编译报错 int r x; // ❌ 编译报错 };类内缺省值不是赋值是初始化区分struct A { int a 10; // 初始化 A(){ a 20; } // 先初始化为10随后赋值修改为20低效尽量避免 };模板类你的listNodeT完全支持类内缺省值写法一样。1.3 defult和deleteC11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数但是因为一些原因这个函数没有默认生成。比如我们提供了拷贝构造就不会生成移动构造了那么我们可以使用 default 关键字显式指定移动构造生成。如果想要限制某些默认函数的生成在 C98 中是该函数设置成 private并且只声明不定义这样只要其他人想要调用就会报错。在 C11 中更简单只需在该函数声明加上delete即可该语法指示编译器不生成对应函数的默认版本称delete修饰的函数为删除函数。1.4 final和override1、override作用显式声明这个函数是用来重写父类虚函数的让编译器帮忙检查语法是否合法防止写错函数签名导致没有成功重写。使用位置只能写在虚成员函数声明末尾class Base { public: virtual void func() { } }; class Derive : public Base { public: // ✅ 正确明确重写父类虚函数 void func() override { } // ❌ 报错父类没有 void func(int)重写失败override直接揪出错误 void func(int) override { } };核心要点只能修饰虚函数普通函数写 override → 编译报错不会改变程序行为仅做编译期检查强烈建议所有重写虚函数都加上 override规避笔误参数、返回值、const、引用限定只要和父类虚函数不一致就无法构成重写变成隐藏overload/hideoverride 会直接报错提醒。class Base { public: virtual void func() const {} }; class Derive : public Base { public: // ❌ 缺少const不是重写override触发编译错误 void func() override {} };2、finalfinal 有两种用法用法 1修饰虚函数含义这个虚函数不能被子类继续重写class Base { public: virtual void func() final { } }; class Derive : public Base { public: void func() override // ❌ 编译报错func被final禁止重写 { } };用法 2修饰类写在类名后面含义该类禁止被继承不能当父类class Base final { }; class Derive : public Base // ❌ 报错Base被final禁止继承 { };注意区分位置void func() final; // final修饰函数禁止子类重写 class A final {}; // final修饰类禁止被继承关键字作用对象核心功能override派生类虚函数告诉编译器我要重写父类虚函数帮忙校验签名final1. 基类虚函数2. 类函数禁止子类重写类禁止被继承二.lambda2.1lambda表达式语法及应用2.1.1lambda的表达是语法Lambda 匿名函数对象编译器遇到[]自动生成一个重载了operator()的类创建这个类的临时对象就是 lambda。没有原生类型只能用auto、std::function、模板接收。lambda 表达式本质是一个匿名函数对象跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使用层而言没有类型所以我们一般是用 auto 或者模板参数定义的对象去接收 lambda 对象。lambda 表达式的格式[capture-list] (parameters)- return type { function boby }[capture-list]捕捉列表该列表总是出现在 lambda 函数的开始位置编译器根据[]来判断接下来的代码是否为 lambda 函数捕捉列表能够捕捉上下文中的变量供 lambda 函数使用捕捉列表可以传值和传引用捕捉具体细节 7.2 中我们再细讲。捕捉列表为空也不能省略。(parameters)参数列表与普通函数的参数列表功能类似如果不需要参数传递则可以连同()一起省略-return type返回值类型用追踪返回类型形式声明函数的返回值类型没有返回值时此部分可省略。一般返回值类型明确情况下也可省略由编译器对返回类型进行推导。{function boby}函数体函数体内的实现跟普通函数完全类似在该函数体内除了可以使用其参数外还可以使用所有捕获到的变量函数体为空也不能省略。lambda 能不能递归直接不行lambda 内部不能调用自身因为定义之前变量还不存在。 两种解决方案使用std::functionfunctionint(int) fib [](int n){ if(n1) return 1; return fib(n-1)fib(n-2); };2.2.2lambda的应用常见实战场景配合算法库sort排序vectorint v{3,1,2}; sort(v.begin(),v.end(),[](int a,int b){ return ab; //降序 });#includealgorithm struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 // ... Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) { } }; struct Compare1 { bool operator()(const Goods gl, const Goods gr) { return gl._price gr._price; } }; struct Compare2 { bool operator()(const Goods gl, const Goods gr) { return gl._price gr._price; } }; int main() { vectorGoods v { { 苹果, 2.1, 5 }, { 香蕉, 3, 4 }, { 橙子, 2.2, 3}, { 菠萝, 1.5, 4 } }; // 类似这样的场景我们实现仿函数对象或者函数指针支持商品中 // 不同项的比较相对还是比较麻烦的那么这里lambda就很好用了 // 价格升序 //sort(v.begin(), v.end(), Compare1()); // 价格降序 //sort(v.begin(), v.end(), Compare2()); 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 g1._evaluate g2._evaluate; }); sort(v.begin(), v.end(), [](const Goods g1, const Goods g2) { return g1._evaluate g2._evaluate; }); return 0; }2.2捕捉列表lambda 表达式中默认只能用 lambda 函数体和参数中的变量如果想用外层作用域中的变量就需要进行捕捉第一种捕捉方式是在捕捉列表中显式的传值捕捉和传引用捕捉捕捉的多个变量用逗号分割。[x, y, z]表示 x 和 y 值捕捉z 引用捕捉。第二种捕捉方式是在捕捉列表中隐式捕捉我们在捕捉列表写一个表示隐式值捕捉在捕捉列表写一个表示隐式引用捕捉这样我们 lambda 表达式中用了那些变量编译器就会自动捕捉那些变量。第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显式捕捉。[, x]表示其他变量隐式值捕捉x 引用捕捉[, x, y]表示其他变量引用捕捉x 和 y 值捕捉。当使用混合捕捉时第一个元素必须是或并且混合捕捉时后面的捕捉变量必须是值捕捉同理混合捕捉时后面的捕捉变量必须是引用捕捉。lambda 表达式如果在函数局部域中他可以捕捉 lambda 位置之前定义的变量不能捕捉静态局部变量和全局变量静态局部变量和全局变量也不需要捕捉lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置捕捉列表必须为空。默认情况下lambda 捕捉列表是被 const 修饰的也就是说传值捕捉过来的对象不能修改mutable 加在参数列表的后面可以取消其常量性也就是说使用该修饰符后传值捕捉的对象就可以修改了但是修改还是形参对象不会影响实参。使用该修饰符后参数列表不可省略 (即使参数为空)。三.包装器3.1function包装器std::function通过类型擦除把不同类型的可调用对象函数指针、lambda、仿函数、bind 结果包装成同一个std::functionR(Args...)类型对外提供统一的operator()调用接口。std::function是一个类模板也是一个包装器。std::function的实例对象可以包装存储其他的可以调用对象包括函数指针、仿函数、lambda、bind 表达式等存储的可调用对象被称为std::function的目标。若std::function不含目标则称它为空。调用空std::function的目标导致抛出std::bad_function_call异常。以上是function的原型他被定义在functional头文件中。std::function - cppreference.com 是 function 的官方文件链接。函数指针、仿函数、lambda 等可调用对象的类型各不相同std::function的优势就是统一类型对他们都可以进行包装这样在很多地方就方便声明可调用对象的类型下面的第二个代码样例展示了std::function作为 map 的参数实现字符串和可调用对象的映射表1. 将可调用对象存入容器vector/maplambda、仿函数各自类型独一无二不能直接放进容器std::function是固定模板类型可以存进vector、map实现函数映射表、事件分发。 示例mapstring, functionvoid() opMap; opMap[add] [](){cout 加法;}; opMap[sub] [](){cout 减法;};2. 实现 Lambda 递归lambda 定义前自身变量尚未创建内部不能直接调用自己。 借助std::function中转实现递归functionint(int) fib; fib [](int n){ if(n 2) return 1; return fib(n-1)fib(n-2); };3.2bind包装器bind 是一个函数模板它也是一个可调用对象的包装器可以把他看做一个函数适配器对接收到的 fn 可调用对象进行处理后返回一个可调用对象。bind 可以用来调整参数个数和参数顺序。bind 也在functional这个头文件中。调用 bind 的一般形式auto newCallable bind(callable,arg_list);其中 newCallable 本身是一个可调用对象arg_list 是一个逗号分隔的参数列表对应给定的 callable 的参数。当我们调用 newCallable 时newCallable 会调用 callable并传给它 arg_list 中的参数。arg_list 中的参数可能包含形如_n的名字其中 n 是一个整数这些参数是占位符表示 newCallable 的参数它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置_1为 newCallable 的第一个参数_2为第二个参数以此类推。_1/_2/_3....这些占位符放到 placeholders 的一个命名空间中。#includefunctional #includeiostream using namespace std; using placeholders::_1; using placeholders::_2; using placeholders::_3; int Sub(int a, int b) { return (a - b) * 10; } int SubX(int a, int b, int c) { return (a - b - c) * 10; } class Plus { public: static int plusi(int a, int b) { return a b; } double plusd(double a, double b) { return a b; } }; int main() { auto sub1 bind(Sub, _1, _2); cout sub1(10, 5) endl; // bind 本质返回的一个仿函数对象 // 调整参数顺序不常用 // _1代表第一个实参 // _2代表第二个实参 // ... auto sub2 bind(Sub, _2, _1); cout sub2(10, 5) endl; // 调整参数个数 常用 auto sub3 bind(Sub, 100, _1); cout sub3(5) endl; auto sub4 bind(Sub, _1, 100); cout sub4(5) endl; // 分别绑死第123个参数 auto sub5 bind(SubX, 100, _1, _2); cout sub5(5, 1) endl; auto sub6 bind(SubX, _1, 100, _2); cout sub6(5, 1) endl; auto sub7 bind(SubX, _1, _2, 100); cout sub7(5, 1) endl; // 成员函数对象进行绑死就不需要每次都传递了 functiondouble(Plus, double, double) f6 Plus::plusd; Plus pd; cout f6(move(pd), 1.1, 1.1) endl; cout f6(Plus(), 1.1, 1.1) endl; // bind一般用于绑死一些固定参数 functiondouble(double, double) f7 bind(Plus::plusd, Plus(), _1, _2); cout f7(1.1, 1.1) endl; // 计算复利的lambda auto func1 [](double rate, double money, int year)-double { double ret money; for (int i 0; i year; i) { ret ret * rate; } return ret - money; }; // 绑死一些参数实现出支持不同年利率不同金额和不同年份计算出复利的结算利息 functiondouble(double) func3_1_5 bind(func1, 0.015, _1, 3); functiondouble(double) func5_1_5 bind(func1, 0.015, _1, 5); functiondouble(double) func10_2_5 bind(func1, 0.025, _1, 10); functiondouble(double) func20_3_5 bind(func1, 0.035, _1, 30); cout func3_1_5(1000000) endl; cout func5_1_5(1000000) endl; cout func10_2_5(1000000) endl; cout func20_3_5(1000000) endl; return 0; }std::bind作用函数适配器可以固定参数、调换参数顺序、减少调用时传入参数数量_1、_2占位符std::placeholders命名空间代表调用 bind 返回对象时传入的第 1、2 个实参绑定普通成员函数必须额外传入类对象 / 对象指针成员函数隐含thisbind 返回类型极其复杂通常使用auto或者std::function接收典型业务场景把一部分参数预先绑定生成签名更简单的回调函数示例里的复利计算器下面是bind的重要用途
返回列表