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