C++终章:探讨C++ 11 新标准
一、前述
此为《C++ Primer Plus(第6版)》一书的终章,本章对前面学习的一些C++11新性能做了提要总结,并针对移动语义、包装器、lambda表达式等新性能做了专门的拓展和介绍,本白在下文中也会针对这些一一做简单的回顾,简单的则一笔带过~
二、C++新标准:内容回顾
2.1 统一初始化
统一初始化则是在参数初始化采用初始化列表{}
的形式进行,进行/不进行=
;初始化列表可使用于基本数据类型、new与类对象。必须注意初始化列表的方法在数据长度缩短时不支持类型转化:
char a = 1234324234;//warning char a = {1234324234};//error!从int到char需要收缩转换!
C++11针对初始化列表的方式专门提供了std::initializer_list
方法,常见的String,Vector都支持。拿vector举例,查看STL源码,从vector的构造函数编写中可以看出初始化列表也支持begin()
和end()
,并且此方法只能在vector类构造函数中使用:
2.2 声明
2.2.1 decltype
decltype这将会把变量的类型定义为表达式指定的类型:
int a; double b; decltype(a) c;//int decltype(a*b) c;//double
2.2.2 auto与后置声明
首先来说又爱又恨的auto,参阅过一些大佬的文章,总结下来auto提高了编程的便利性,但是auto增加了代码的漏洞和可读性,就像是一把双刃剑!个人目前也只是在迭代器中用到了auto,有个比较好玩的事情是,很多大佬都认为,当C++50的时候,C++将进化为:
#inlclude <iostream> int main(void) { auto; }
不开玩笑,参考各位大佬的建议,使用auto时最好使用后置返回,这样会让人看的很爽:
auto sum_value() ->long double; auto sum_value(int a) ->decltype(a);
代码中sum_value
的第二种声明中使用了后置语法,后置语法部分人认为这是C++倒退的体现,但是正好可以弥补auto声明不宜阅读的缺点。
2.2.4 模板别名using
给迭代器等标识符起别名,我感觉没用~这样会降低可读性~
using itype = vector<int>::iterator;
2.3 智能指针
智能指针主要是为了自动处理堆内存而设置的。拿string来说,如果足够仔细和谨慎,在每一个string类对象使用完毕时都合理delete,那就不需要智能指针~可惜人无完人。智能指针一般常指:unique_ptr
、shared_ptr
、auto_ptr
。从书中的示例图可以看出,智能指针可以保证类对象释放时对应的内存空间也释放,这在后文逐一介绍:
三种智能指针中auto_ptr,这已经被遗弃;unique_ptr
使用于一个智能指针指向一个类对象的场景;shared_ptr
使用于当多个指针指向同一个类对象的场景,这是因为当多个指针指向同一个类对象时,会发生同一内存空间被多次释放的问题,因为基于unique_ptr
发展了更高级的指针shared_ptr
,shared_ptr
具备记录类对象是否过期的功能。除此之外,为了避免多次释放,也可以采用记录所有权或执行深拷贝的方式实现。
2.4 异常规范的修改
加入了noexpect
来指明函数不抛出异常~
void sum_value() noexcept;//此函数内部不会抛出异常
2.5 作用域枚举
当使用传统枚举时,我们必须注意同一作用域内枚举名称重复的情况,C++为枚举值也引入了作用域的概念,这样在使用时就不会发生名称冲突:
enum class ar { aa, bb }; enum class ac { aa, bb }; ar::aa; ac::aa; enum struct af { aa, bb };//同class enum struct ag { aa, bb }; af::aa; ac::aa;
2.6 对类的修改
显示转换运算符explicit
,声明类转换函数只支持显式转换
初始化成员列表
2.7 右值引用
支持引用右值
三、C++新标准:移动语义
3.1 基础实现
移动语义用于移动,深/浅拷贝用于复制。对于转移数据的场景来说移动语义(转交所有权)的操作更快!何时需要移动,何时需要拷贝赋值呢?这就是右值引用的作用。在右值赋值左值进行移动的优越性远大于先创建临时对象再赋值。来一起看一下移动语义的实现:
示例~
#include <iostream> #include<string> using namespace std; class cpmv { public: struct info { string qcode; string zcode; }; public: cpmv(); ~cpmv(); cpmv(string q,string z); cpmv(const cpmv &cp); cpmv(cpmv&& mv); cpmv& operator= (const cpmv& cp); cpmv& operator= (cpmv && mv); cpmv operator+(const cpmv &obj) const; void display() const; private: info* pi; }; //默认构造-sj cpmv::cpmv() { cout << "默认构造:cpmv()" << endl; this->pi = nullptr; } //默认析构-sj cpmv::~cpmv() { cout << "默认析构:~cpmv()" << endl; delete this->pi; } //自定义构造函数-sj cpmv::cpmv(string q, string z) { cout << "构造函数:cpmv(string q, string z)" << endl; this->pi = new info; pi->qcode = q; pi->zcode = z; } //复制构造函数-sj cpmv::cpmv(const cpmv& cp) { cout << "构造函数:(const cpmv& cp)" << endl; this->pi = new info; pi->qcode = cp.pi->qcode; pi->zcode = cp.pi->zcode; } //移动构造函数-sj cpmv::cpmv(cpmv&& mv) { cout << "移动构造函数:cpmv(cpmv&& mv)" << endl; this->pi=mv.pi;//移交所有权 mv.pi == nullptr; } //赋值运算符重载-sj cpmv& cpmv::operator= (const cpmv& cp) { cout << "=重载:operator= (const cpmv& cp)" << endl; if (cp.pi == this->pi) return *this; delete this->pi;//释放本地的空间,重新新建 this->pi = new info; pi->qcode = cp.pi->qcode; pi->zcode = cp.pi->zcode; return *this; } //赋值运算符移动构造-sj cpmv& cpmv::operator= (cpmv&& mv) { cout << "移动=重载:operator= (cpmv&& cp)" << endl; if (mv.pi == this->pi) return *this; this->pi = mv.pi;//移交所有权 mv.pi = nullptr; return *this; } //加法运算符重载-sj cpmv cpmv::operator+(const cpmv& obj) const { cout << "+重载:operator+(const cpmv& obj) const" << endl; return cpmv(this->pi->qcode += obj.pi->qcode, this->pi->zcode += obj.pi->zcode); } int main(void) { cpmv pv1("zhangwwei", "a2222"); cpmv pv2("lalal", "lalal"); cout << "-----------=移动构造----------------" << endl; cpmv pv3; pv1 = pv2 + pv1; return 0; }
移动语义一般需要编写两个函数,分别是移动构造函数和赋值运算符的移动构造;但是如果没有自定义,编译器将只提供一个默认移动构造函数。
3.2 强制移动
对于某些场景,我们相对左值进行移动构造时,可以采用static_const
进行强制类型转换,也可以使用utility类库中的move()
函数进行。
四、C++新标准:Lambda表达式
归根结底还是表达式,我们做简单运算时可以使用。当然部分场景也可以用三目运算、自定义函数/函数指针、函数运算符替代。本白看来lambda表达式是否使用完全取决于个人爱好:
示例~
#include <iostream> #include <vector> #include <cstdlib> #include <ctime> #include <algorithm> using namespace std; #define SIZE1 39000 int main(void) { vector<int> numbers(SIZE1); srand(time(0)); //产生新元素,随机产生 generate(numbers.begin(), numbers.end(), rand); cout << "sample size is " << SIZE1 << endl; //判断统计满足if的个数 int count3 = count_if(numbers.begin(), numbers.end(), [](int x) {return x % 3 == 0; });//lambda表达式 cout << "Counter divisible by 3 is " << count3 << endl; //判断统计满足if的个数 int count13 = count_if(numbers.begin(), numbers.end(), [](int x) {return x % 13 == 0; }); cout << "Counter divisible by 13 is " << count13 << endl; cout << "Lambda 表达式完全体----" << endl; count3 = 0; count13 = 0; //完全体,需要修改参数时,将参数地址放入[] for_each(numbers.begin(), numbers.end(), [&count3](int x) {count3 += (x % 3 == 0); }); cout << "Counter divisible by 3 is " << count3 << endl; for_each(numbers.begin(), numbers.end(), [&count13](int x) {count13 += (x % 13 == 0); }); cout << "Counter divisible by 3 is " << count13 << endl; return 0; }
五、新的类功能
5.1 特殊成员函数
编写类时,普遍来看public中最少应具备四个成员函数:默认构造函数、复制构造函数、赋值运算符重载、析构函数。C++11针对右值新增了两个:移动构造、移动复制构造。当用户又没自定义时,C++将提供默认的移动构造函数和移动复制构造函数:
SomeClass::SomeClass (const SemoClass &);//默认复制构造 SomeClass::SomeClass (SemoClass &&);//默认移动构造 SomeClass & SomeClass::SomeClass ::Operator = (const SemoClass &);//默认赋值运算 SomeClass & SomeClass::SomeClass::Operator = (SemoClass &&);//默认的移动赋值运算
5.2 成员函数的控制
如5.1,我们编写了共计6种基本函数,但是我们只想使用使用部分函数时,比如禁止复制对象,那么可以采用delete
方法。除此之外,还可以使用default
来将方法声明为默认,此时编译器将不会提供对应的默认函数,使用如下:
SomeClass::SomeClass (const SemoClass &) = default;//默认复制构造 SomeClass::SomeClass (SemoClass &&) = delete;//默认移动构造
5.3 委托构造和继承构造
委托构造:同一个类中,当多个构造函数之间的代码可以复用,也就是存在多个构造函数相互调用时,C++支持在一个构造函数调用前面编写的构造函数,使用采用初始化列表语法的变种实现:
继承构造:派生类中,派生类可以继承基类的构造函数的方法,此时当定义派生类对象时,将根据特征标匹配不同的构造函数,使用using
声明实现:
注意:如果派生类的构造函数与基类的构造函数特征标相同,将优先调用基类的构造函数!
5.4 管理虚方法:override和final
虚函数对于实现多态至关重要,但虚方法也为编程带来了一些陷阱,比如派生类和基类的同一虚方法特征标完全相同时,派生类的的虚方法将覆盖基类的虚方法,除非显式调用。针对这一bug,C++提出了override/final
标识符,即此种情况下,若您想让派生类覆盖基类的虚方法,需要在派生类中虚方法后添加override
;当您想禁止覆盖时,使用final
。
class Act { public: virtual fun1(char ch) const; public: virtual fun2(char ch) const; } class Aer : pbulic Act { public: virtual fun1(char ch) const override;//覆盖 public: virtual fun2(char ch) const final;//不覆盖 }
五、C++新标准:包装器
Warpper包装器的提出是为了避免返回参数类型相同,但函数模板多次实例化导致性能损耗的场景。比如:
template <typename T,typename F> T use_f(T v, F f) { static int count = 0;//所有的模板函数共用一个count count++;//每实例化一个函数,count++ cout << "USE_F count is " << count << ", &count is " << &count << endl; return f(v); }
运行:
#include "包装器.h" double dub(double x) { return 2.0 * x; }; double squ(double x) { return 2.0 * x; }; int main(void) { double y = 1.21; //第一中用法 :函数指针 double u = use_f(y,dub/*函数指针*/); cout << "第一种:" << u << endl; u = use_f(y, squ/*函数指针*/); cout << "第一种:" << u << endl;//实例化的是同一目标函数 //第三种写法:lambda表达式,每个都会创建新对象 u = use_f(y, [](double u) {return u * u; }); cout << "第二种:" << u << endl;//实例化的是同一哥目标函数 u = use_f(y, [](double u) {return u * u; }); cout << "第二种:" << u << endl;//实例化的是同一哥目标函数 return 0; }
可以看出,不同类型的F f
将导致函数模板重新实例化(不同的lambda表达式也会重新实例化),count值一直重新置1,这就造成了资源浪费。而当我们使用包装器时:
#include <funtional> funtion <返回值(特征标)> 包装器名称 = 代表的“模板参数”具体类型(类的函数符/函数指针/lambda等)
#include "包装器.h" double dub(double x) { return 2.0 * x; }; double squ(double x) { return 2.0 * x; }; int main(void) { double y = 1.21; //第一中用法 :函数指针 double u = use_f(y,dub/*函数指针*/); cout << "第一种:" << u << endl; u = use_f(y, squ/*函数指针*/); cout << "第一种:" << u << endl; //第三种写法:lambda表达式,每个都会创建新对象 u = use_f(y, [](double u) {return u * u; }); cout << "第二种:" << u << endl; u = use_f(y, [](double u) {return u * u; }); cout << "第二种:" << u << endl; cout << "---------------包装器-------------------" << endl; function<double(double)> ef1 = dub;//包装器1,指向dub function<返回值(特征标)> function<double(double)> ef2 = squ;//包装器2 function<double(double)> ef3 = [](double u) {return u * u; };//包装器3 //包装器,当每个指向不同类对象的包装器的接收参数double和返回参数double与前面的一个包装器重复时,就会共用函数模板 u = use_f(y, ef1); cout << "包装器1:" << u << endl; u = use_f(y, ef2); cout << "包装器2:" << u << endl; u = use_f(y, ef3); cout << "包装器3:" << u << endl; return 0; }
当使用包装器时,函数模板将只进行一次实例化,因此只要在包装器中将不同参数和返回值进行一一打包后,函数模板调用包装器时会先检查包装器对应的function<返回值(特征标)>
是否已经存在,不存在才会实例化~
六、C++新标准:可变参数模板(超级函数模板)
常规函数模板只能接受固定数量参数,但是当参数数量变化,函数的基本功能(比如求和)不变时,我们还需要去再编写一个模板吗?那参数有1-1000种呢?因此提出了可变参数模板:可以接受不同数量的参数。实现可变参数模板的核心思想便是递归:
void show_list() //应该使函数模板作为最后一次的终止条件 { cout << "终止!!" << endl; };//相当于终止条件 //模板和函数参数包 template <typename T,typename... Args/*模板参数包*/> void show_list(const T &value,const Args&... args/*函数参数包*/) { cout << "Vaule = " << value << endl; //递归展开参数包,对形参列表的第一项和value进行对象,剩余项继续在args递归 show_list(args...);//前进条件 }
运行:
int main(void) { int n = 14; double x = 2.12314; string mr = "Mr mick!"; show_list(n, x, mr); show_list(n,x); return 0; }
可以看出可变参数模板是将Args&
存放的参数包逐个遍历实现的。如果我们想对其中某一步进行特定的处理,比如在剩余两个参数时,可以再定义一个函数模板的重载,需要注意这将导致可变参数的递归停止!!
template <typename T, typename F> void show_list(const T &value,const F & YY) { cout << "--> 剩余两个参数 " << value << " " <<YY<< endl; };
七、学习结语
学渣本渣~2023,冲!!!!
本文来自博客园,作者:{张一默},转载请注明原文链接:https://www.cnblogs.com/YiMo9929/p/17066878.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)