Fork me on GitHub

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_ptrshared_ptrauto_ptr从书中的示例图可以看出,智能指针可以保证类对象释放时对应的内存空间也释放,这在后文逐一介绍:

三种智能指针中auto_ptr,这已经被遗弃;unique_ptr使用于一个智能指针指向一个类对象的场景;shared_ptr使用于当多个指针指向同一个类对象的场景,这是因为当多个指针指向同一个类对象时,会发生同一内存空间被多次释放的问题,因为基于unique_ptr发展了更高级的指针shared_ptrshared_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,冲!!!!

posted @ 2023-01-29 18:34  张一默  阅读(134)  评论(0编辑  收藏  举报