ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

【C++】13. C++11新特性【上】

【C++】13. C++11新特性【上】 目录C11简介一、列表初始化1.大括号初始化2. initializer_list二、右值引用和移动语义1. 左值和右值2. 左值引用和右值引用3.右值引用的的意义4.移动构造和移动赋值1. 为什么需要移动语义先看拷贝的代价2. 移动构造函数3. 移动赋值函数4. 显式声明 default / delete5.引用折叠5.完美转发1. 问题普通转发会丢失右值属性2. 解决用 forward 还原左右值属性3. forward 为什么能做到三件套原理三、lambdalambda 表达式各部分说明捕捉列表①显示捕捉②隐式捕捉③混合捕捉捕捉列表的其他要点C11简介C11 是 C 发展过程中一次具有里程碑意义的版本更新。相比之前的 C98标准C11 不仅对原有语言特性进行了完善还加入了大量新的语法特性和标准库组件使 C 在保持高性能、灵活性以及底层控制能力的同时代码编写也变得更加简洁、高效和现代化。C11 能更好地用于系统开发和库开发、语言更加泛华和简单化、更加稳定和安全不仅功能更强大而且能提升程序员的开发效率公司实际项目开发中也用得比较多。一、列表初始化1.大括号初始化在c98中允许数组 和结构体使用大括号初始化例如C11想统一初始化方式试图实现一切对象皆可用{}初始化{}初始化也叫做列表初始化。内置类型支持自定义类型也支持。自定义类型本质是类型转换中间会产生临时对象依赖于构造函数最后优化了以后变成直接构造。{}初始化的过程中可以省略掉。2. initializer_list上面的初始化已经很方便了但是对象容器初始化还是不太方便比如一个 vector 对象我想用N个值去构造初始化那么我们得实现很多个构造函数才能支持。所以这是很不方便的。C11库中提出了一个 std::initializer_list 的类这个类的本质是底层开一个数组将数据拷贝过来std::initializer_list 内部有两个指针分别指向数组的开始和结束。std::initializer_list 支持迭代器遍历。#include iostream #include vector #include list #include map using namespace std; int main() { vectorint v { 1,2,3,4,5 }; listdouble l { 3.6,5.9,4.8 }; mapstring, int m { {hello, 1}, {nihao, 3} }; return 0; } //这里赋值使用的大括号及其内部元素即是initializer_list。二、右值引用和移动语义1. 左值和右值左值一般是一个持久存在的表达式有明确内存地址可以进行取地址操作。右值指那些临时性的即将被销毁的以及常量表达式没有明确的内存地址不能进行取地址操作。2. 左值引用和右值引用左值引用是给左值取别名右值引用是给右值取别名。举个例子int main() { int a 0; int r1 a; //左值引用 int r2 1; //右值引用 return 0; }左值引用不能直接引用右值需要加上const右值引用不能直接引用左值需要将左值使用move函数包装起来move将左值转化为右值引用。示例int main() { const int r1 1; //左值引用引用右值 int a 0; //右值引用引用左值 int r2 (int)a; //强转 int r3 std::move(a); //使用move函数 return 0; }3.右值引用的的意义const左值引用和右值引用都可以延长临时对象的生命周期直至引用的生命周期结束但const左值引用所引用的对象不可被修改而通过右值引用可以修改所引用的临时对象因为右值引用本身是左值。int main() { const int r1 1 3; r1; // 报错表达式必须是可修改的左值 int r2 1 3; r2; // 可以修改 return 0; }func1使用左值引用传参可以减少对象拷贝提高效率。func2也可以返回左值引用但要注意对象的生命周期如果引用的是函数内部的局部对象那么函数执行结束后对象就被销毁了返回的引用也就失效了因此这种情况不能使用左值引用返回。str是函数内部的局部对象函数结束后就会被销毁。因此这里不能使用左值引用返回否则返回的引用会指向一个已经被销毁的对象形成悬空引用。这里使用按值返回是没有问题的。局部对象出作用域就会销毁所以会拷贝构造一个临时对象然后再拷贝构造给ret。不过vs2022编译器会优化连续的构造、拷贝构造会优化为直接构造。4.移动构造和移动赋值原来 C 类中有 6 个默认成员函数构造函数析构函数拷贝构造函数拷贝赋值重载取地址重载const 取地址重载。而 C11 新增了两个移动构造函数和移动赋值运算符重载。移动构造的本质是指针转移把别人的资源窃过来不是再深拷贝一份——适合临时对象右值性能提升巨大。1. 为什么需要移动构造先看拷贝的代价std::string s1 一串很长的字符串...; // 假设底层 char[1000000] std::string s2 s1; // 深拷贝开新空间、复制 100w 字节 std::string s3 std::move(s1); // 移动s1 把指针给s3自己置空 // 结果s1 s2 和 s3 各持一份数据但 s3 这份是窃来的没花复制成本2. 移动构造函数构造函数的一种重载专门处理用右值初始化新对象。class String { private: char* _str; size_t _size; size_t _capacity; public: // 移动构造窃取——把别人指针拿过来 String(String s) { // 第一个参数必须是右值引用 T _str s._str; // 1. 直接把对方的指针拿过来 _size s._size; _capacity s._capacity; s._str nullptr; // 2. 关键一步把对方置空防止它析构时把资源也释放掉 } ~String() { delete[] _str; } };3. 移动赋值函数已存在的对象 另一个对象分两种情况左值用拷贝赋值operator右值用移动赋值operator。// 移动赋值参数用值传递内部 swap天然异常安全 String operator(String s) { // 形参 s 是实参的拷贝这里要触发移动构造 swap(s); // 1. 把 this 的资源和 s 交换 return *this; // 2. s 离开作用域时析构的是 this 原来的资源 } // 移动版C11 之后常写为更明显的右值引用版本 String operator(String s) { if (this ! s) { // 自赋值检查 delete[] _str; // 先释放自己原有的资源 _str s._str; _size s._size; _capacity s._capacity; s._str nullptr; } return *this; }你没写析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值中的任何一个编译器就不再自动生成移动构造和移动赋值。反过来你只写了其他四个析构/拷贝构造/拷贝赋值/普通赋值没碰移动相关的编译器会默认生成移动版本。为什么这么反直觉因为移动是窃资源如果你的类需要自己写析构说明有资源要管或者写拷贝构造说明深拷贝逻辑特殊编译器就不敢默认生成移动版本怕行为对不上。4. 显式声明 default / delete写法含义String(String) default;强制编译器生成默认移动构造即便你写了析构String operator(String) default;同上移动赋值String(String) delete;显式禁止移动构造std::move(s)编译报错String operator(String) delete;同上移动赋值 default常用于我虽然写了析构/拷贝但移动我想要编译器默认行为就够了的场景 delete常用于我这个类不能被移动资源特殊的禁止场景。5.引用折叠C 规定引用不能引用引用不存在引用的引用所以当模板推导出T 、T 这种引用套引用的类型时编译器要按规则折叠成一个引用——这就是引用折叠。template typename T void f(T x) { } int a 10; f(a); // 传左值 → T 推导为 int → 参数是 int → 折叠为 int f(std::move(a)); // 传右值 → T 推导为 int → 参数是 int → 保持右值引用5.完美转发完美转发保留参数的左/右值属性——传进来是左值就给下家左值是右值就给下家右值既不丢也不改。1. 问题普通转发会丢失右值属性std::move把a变成了右值但一旦进入 wrapperarg就是一个有名字的变量C 标准有名字的就是左值右值属性就丢了。这就是为什么要完美转发。2. 解决用 forward 还原左右值属性3. forward 为什么能做到三件套原理①模板形参T是万能引用forwarding reference——它能根据实参类型自动推导传左值a→T推导为int传右值10或move(a)→T推导为int② forward 内部靠引用折叠把 T 还原// std::forward 的实现本质C11/14简化版 template class T T forward(typename remove_referenceT::type arg); // C11 完整版还有第二个重载用于显式 T 时传右值场景 template class T T forward(typename remove_referenceT::type arg);三、lambdalambda 是 C11 引入的匿名函数对象可以写在任何需要一段可调用逻辑的地方——编译器在底层会把它翻译成一个带operator()的匿名类即仿函数捕获的变量变成这个匿名类的成员。lambda 表达式书写格式[capture-list] (parameters) mutable - return-type { statement }lambda 表达式各部分说明[capture‑list]捕捉列表该列表总是出现在 lambda 函数的开始位置编译器根据[]来判断接下来的代码是否为 lambda 函数捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。(parameters)参数列表与普通函数的参数列表一致如果不需要参数传递则可以连同()一起省略。mutable默认情况下lambda 函数总是一个const函数mutable可以取消其常量性。使用该修饰符时参数列表不可省略即使参数为空。-return‑type返回值类型用追踪返回类型形式声明函数的返回值类型没有返回值时此部分可省略。返回值类型明确情况下也可省略由编译器对返回类型进行推导。{statement}函数体在该函数体内除了可以使用其参数外还可以使用所有捕获到的变量。#include iostream using namespace std; int main() { //简单的lambda表达式 auto f [](int x, int y)-int {return x y; }; //调用 cout f(1, 2) endl; return 0; }在定义lambda表达式时我们要遵守如下规则1. 捕捉列表的“[ ]”不能省略。2. lambda表达式的参数为空时参数列表包括“( )”可以省略。3. 返回值类型包括“-”可以省略。4. 若返回值类型已写那么就算没有参数参数列表的“( )”也不能省略。5. 函数体的“{ }”不能省略。捕捉列表lambda表达式的函数内部只能使用参数列表的变量这时要想使用外部变量就需要依靠捕捉列表了。捕捉列表分为三种显示捕捉隐式捕捉混合捕捉。①显示捕捉显示捕捉就是在捕捉列表里写自己要使用的变量。int main() { int a 1; int b 3; auto f [a](int x) { //return a b x; // 报错b不能使用 return a x; //捕捉列表中写明了a可以使用a }; cout f(1) endl return 0; } 这里a的值不能修改因为是传值捕捉若要修改就得变成传引用捕捉[a]了②隐式捕捉隐式捕捉就显得十分便利它的机制是函数体内部使用了哪些变量就捕捉哪些变量。自动捕捉auto f1 [] {}; // 隐式的传值捕捉auto f2 [] {}; // 隐式的传引用捕捉③混合捕捉如果我们需要在隐式捕捉的基础上确保其中某些值可以被修改或不能被修改那么就可以使用混合捕捉。auto f1 [, a] {}; // 其他变量使用传值捕捉a使用传引用捕捉 auto f2 [, a] {}; // 其他变量使用传引用捕捉a使用传值捕捉混合捕捉列表的第一个位置必须是或。捕捉列表的其他要点全局变量和静态变量不需要也不能通过捕捉列表进行捕捉。Lambda 表达式可以在作用域允许的情况下直接访问它们。对于值捕捉Lambda 会将外部变量的值复制一份保存到 Lambda 对象内部。默认情况下这些通过值捕捉得到的成员不能在 Lambda 函数体内修改。如果希望修改 Lambda 内部保存的副本同时不影响外部原变量可以使用mutable关键字。int main() { int a 0; auto f []() mutable - int { a; // 修改的是 Lambda 内部保存的 a 的副本 return a; }; cout f() endl; // 1 cout a endl; // 0外部的 a 没有改变 return 0; }注意在 C11C20 中使用mutable时即使 Lambda 没有参数也需要写出空参数列表()。
返回列表