C++进阶第二十五篇——C++11(列表初始化+变量类型推导+右值引用和移动语义+新的类功能+可变模板参数)

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++进阶第二十五篇——C++11(列表初始化+变量类型推导+右值引用和移动语义+新的类功能+可变模板参数)相关的知识,希望对你有一定的参考价值。

⭐️接下来的几篇博客,我会给大家介绍C++11的相关内容,今天主要介绍一部分基本语法,有一部分C++11的语法在前面的博客中有介绍过,例如:范围for、final和override关键字和nullptr这样的表示空指针关键字等
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code

目录


🌏C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。此次标准为C++98发布后13年来第一次重大修正。

🌏列表初始化

🌲用法

在C++98中,只能够对数组元素进行统一的列表初始化,但是对应自定义类型,无法使用进行初始化,如下所示:

// 数组类型
int arr1[] =  1,2,3,4 ;
int arr2[6] 1,2,3,4,5,6 ;
// 自定义类型(C++98不支持下面这种初始化的方式)
vector<int> v 1,2,3 ;

在C++11中,扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加,如下所示:

// 内置类型变量
int a 2 ;
int b =  3 ;
int c =  a + b ;

// 动态数组 C++98不支持
int* arr = new int[5] 1,2,3,4,5 ;
// 容器使用进行初始化
// vector<int> v =  1,2,3 ;
vector<int> v 1,2,3 ;// 等号可以省略不写
map<int, int> m 1,1,2,2,3,3 ;

自定义类型的列表初始化:
下面是自己定义的一个类:

class Point

public:
	Point(int x, int y)
		:_x(x)
		,_y(y)
	
	void PrintPoint()
	
		printf("点的坐标为:(%d, %d)\\n", _x, _y);
	
private:
	int _x;
	int _y;
;

创建一个Point类并使用对其进行列表初始化,具体如下:

int main()

	// 自定义类型列表初始化  
	Point p 1, 2 ;
	p.PrintPoint();
	size_t i = 0;
	
	return 0;

代码运行结果如下:

可以看出,列表初始化对应自定义类型都是支持的。

🌲initializer_list


initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值对应多个对象的列表初始化,必须支持一个带有initializer_list类型参数的构造函数。

注意: 这种类型的对象由编译器根据初始化列表声明自动构造,初始化列表声明是用和,

容器使用initializer_list作为构造函数的参数的例子:

  • vector

  • list

  • map

实例演示: 简单模拟一个vector中的构造函数和赋值重载

template<class T>
class Vector

public:
	Vector(initializer_list<T> l)
		:_size(0)
		,_capacity(l.size())
	
		_a = new T[_capacity];
		for (auto e : l)
		
			_a[_size++] = e;
		
	
	Vector<T>& operator=(initializer_list<T> l)
	
		delete _a;
		_size = 0;
		_capacity = l.size();
		_a = new T[_capacity];
		for (auto e : l)
		
			_a[_size++] = e;
		

		return *this;
	
private:
	T* _a;
	size_t _size;
	size_t _capacity;
;

int main()

	Vector<int> v1 =  1,2,3 ;
	Vector<int> v2 =  3,5,7,9 ;
	v2 =  1,2,3 ;
	
	return 0;

🌏变量类型推导

🌲auto类型推导

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推导。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

注意: auto声明的类型必须要进行初始化,否则编译器无法推导出auto的实际类型。auto不能作为函数的参数和返回值,且不能用来直接声明数组类型
实例演示:

int main()

	int a = 10;
	auto pa = &a;
	auto& ra = a;// 声明引用类型要加&

	cout << typeid(a).name() << endl;
	cout << typeid(pa).name() << endl;
	cout << typeid(ra).name() << endl;

	return 0;

代码运行结果如下:

🌲decltype类型推导

decltype是根据表达式的实际类型推演出定义变量时所用的类型。且还可以使用推导出来的类型进行变量声明。
实例演示:

int Add(int x, int y)

	return x + y;

int main()

	int a = 10;
	int b = 20;

	// 用decltype自动推演a+b的实际类型
	decltype(a + b) c = 10;
	cout << typeid(c).name() << endl;
	// 不带参数,推导函数类型
	cout << typeid(decltype(Add)).name() << endl;
	// 带参数,推导函数返回值类型,注意这里不会调用函数
	cout << typeid(decltype(Add(1,1))).name() << endl;

	return 0;

代码运行结果:

注意: decltype不可以作为函数的参数,编译时推导类型

🌲运行时类型识别

C++98中确实已经支持RTTI(运行时类型识别):

  • typeid只能查看类型不能用其结果类定义类型(上面两个例子都使用了)
  • dynamic_cast只能应用于含有虚函数的继承体系中(后面会详细介绍)

注意:

  • 运行时类型识别的缺陷是降低程序运行的效率
  • typeid只能推导类型,但是不能使用类型声明和定义变量

🌏右值引用和移动语义

🌲右值引用

在之前的博客中,我已经介绍过了引用的语法,那里的引用都是左值引用。C++11新增了右值引用的语法特性,给右值取别名。左值引用和右值引用都是给对象取别名,只不过二者对象的特性不同。

注意: 左值引用用符号&,右值引用的符号是&&

左值、左值引用。右值和右值引用:

专有名词概念
左值一个表示数据的表达式,可以取地址和赋值,且左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边,例如:普通变量、指针等,const修饰后的左值不可以赋值,但是可以取地址,所以还是左值
左值引用给左值的引用,给左值取别名 ,例如:int& ra = a;
右值一个表示数据的表达式,右值不能取地址,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等
右值引用给右值的引用,给右值取别名,例如:int&& ra = Add(a,b)

实例1:

int Add(int x, int y)

	return x + y;


int main()

	int a = 10;// 左值,可以取地址
	int& ra = a;// 左值引用

	int&& ret = Add(3, 4);// 函数的返回值是一个临时变量,是一个右值

	return 0;

总结1:

  1. 左值
  • 普通类型的变量,可以取地址
  • const修饰的常量,可以取地址,也是左值
  • 如果表达式运行结果或单个变量是一个引用,则认为是右值
  1. 右值
  • 纯右值:基本类型的常量或临时对象,如:a+b,字面常量
  • 将亡值:自定义类型的临时对象用完自动完成析构,如:函数以值的方式返回一个对象

实例2:

int main()

	int a = 10;
	int& r1 = a;// 左值引用
	
	//int& r2 = 10;// error,左值引用不可以引用右值  (这是因为权限放大了,所以不行)
	const int& r2 = 10;// const的左值引用可以引用右值(这是因为权限不变,所以可以)

	return 0;

总结2:

  • 左值引用不可以引用右值
  • 加了const的左值引用既可以引用左值也可以引用右值

实例3:

int main()

	int&& r1 = 10;// 对字面常量进行引用(右值引用)
	r1 = 20;// 10原本是一个字面常量,无空间存储,右值引用后会开一块空间把值存起来,可以取地址
	cout << &r1 << endl;

	int a = 10;
	// int&& r2 = a;   // error,右值引用不可以引用左值
	int&& r2 = move(a);// move后的左值可以引用,a的属性不变,引用的是move的返回值

	return 0;

总结3:

  • 右值引用不可以引用左值
  • 右值引用可以引用move后的左值

🌲右值引用的移动语义

移动语义: 将一个对象中资源移动到另一个对象中的方式,可以有效减少拷贝,减少资源浪费,提供效率。

问题提出:
先看下面简单的移动代码:

class String

public:
	String(const char* str = "")
		:_str(new char[strlen(str) + 1])
		, _size(0)
	
		strcpy(_str, str);
		_str[_size] = '\\0';
	
	String(const String& s)
		:_str(new char[strlen(s._str) + 1])
		, _size(s._size)
	
		cout << "深拷贝" << endl;
		strcpy(_str, s._str);
	
	String& operator=(String& s)
	
		if (this != &s)
		
			cout << "深拷贝" << endl;
			delete _str;
			_str = new char[strlen(s._str) + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_str[_size] = '\\0';
		

		return *this;
	
	~String()
	
		delete _str;
	
private:
	char* _str;
	size_t _size;
;
String func(String& str)

	String tmp(str);
	return tmp;

int main()

	String s1("123");// 
	String s2(s1);
	String s3(func(s1));

	return 0;

代码运行结果如下:

分析结果:
第一次深拷贝是因为s1拷贝构造s2,这里都不难理解。主要看后两次,s1传参过程不发生深拷贝,因为这里是传引用,接着就是str拷贝构造tmp,这里会发生一次深拷贝,紧接着就是返回tmp,tmp会先拷贝构造一个临时对象(这里会发生一次深拷贝),然后临时对象拷贝构造给s3(这里会发生一次深拷贝),连续两次拷贝构造会被编译器优化成一次,这也就是我们上面看到的两次深拷贝。
分析问题:
在上面的代码中,可以发现,func中的tmp、返回是构造的临时对象和s3都有一个独立的空间,且内容是相同的,这里相当于创建了3个内容完全相同的对象,这是一种极大的浪费,且效率也会降低。
如何解决?
移动语义来解决

这里解决其实就是进行资源的转移,这里加上一份移动构造的代码,如下:

String(String&& s)
:_str(s._str)

	// 对于将亡值,内部做移动拷贝
	cout << "移动拷贝" << endl;
	s._str = nullptr;

代码运行结果如下:

因为返回的临时对象是一个右值,所以会调用上面的移动构造的代码对返回的临时对象进行构造,本质是资源进行转移,此时tmp指向的是一块空的资源。最后返回的临时对象拷贝构造给s3时还是调用了移动构造,两次移动构造被编译器优化为一个。可以看出的是这里解决的是减少接受函数返回对象时带来的拷贝,极大地提高了效率

除了移动构造,我们还可以增加移动赋值,具体如下:

String& operator=(String&& s)

	cout << "移动赋值" << endl;
	_str = s._str;
	_size = s._size;
	s._str = nullptr;

	return *this;

演示:

int main()

	String s1("123");
	String s2("ABC");
	s2 = func(s1);

	return 0;

运行结果如下:

可以看出的是func返回的临时对象是通过移动赋值给s2的,也是一次资源的转义
注意:

  1. 移动构造和移动赋值函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
  2. 在C++11中,编译器会为类默认生成一个移动构造和移动赋值,该移动构造和移动赋值为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造和移动赋值。

总结: 右值引用本身没有多大意义,引入了移动构造和移动赋值就有意义了

右值引用和左值引用减少拷贝的场景:

  1. 作参数时: 左值引用减少传参过程中的拷贝。右值引用解决的是传参后,函数内部的拷贝构造
  2. 作返回值时: 如果出了函数作用域,对象还存在,那么可以使用左值引用减少拷贝。如果不存在,那么产生的临时对象可以通过右值引用提供的移动构造生成,然后通过移动赋值或移动构造的方式将临时对象的资源给接受返回值者

🌲move

move: 当需要用右值引用引用一个左值时,可以通过move来获得绑定到左值上的右值引⽤。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

注意:

  • 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量value不会被销毁。move告诉编译器:我们有⼀个左值,但我们希望像⼀个右值⼀样处理它。
  • STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。

使用如下:

int main()

	String s1("123");
	String s2(move(s1));

	return 0;

代码运行结果如下:

需要知道的是,move后的s1变成了一个右值,所以会调用移动构造去构造s2,这样s1的资源就被转移给了s2,s1本身也没有资源了,如下:

🌲STL容器增加了右值引用

列举一部分:

  • list的尾插

  • vector的尾插

如果要插入的对象是一个纯右值或将亡值,就会调用下面这个版本的插入,如果为左值就会调用上面这个版本的插入。

🌲完美转发和万能引用

  • 完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
  • 万能引用: 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

问题: 右值引用的对象再作为实参传递时,属性会退化为左值,只能匹配左值引用(右值引用后可以取地址,底层会开一块空间把这样的值存起来,所以属性发生了改变)
如下:

void Fun(int& x)  cout << "左值引用" << endl; 
void Fun(const int& x)  cout << "const 左值引用" << endl; 
void Fun(int&& x)  cout << "右值引用" << endl; 
void Fun(const int&& x)  cout << "const 右值引用" << endl; 
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)

	Fun(t);

int main()

	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;

代码运行结果如下: 属性丢失

解决: 使用完美转发能够在传递过程中保持它的左值或者右值的属性

template<typename T>
void PerfectForward(T&& t)

	Fun(std::forward<T>(t));

代码运行结果如下: 属性保持了

总结: 右值引用在传参的过程中移动要进行完美转发,否则会丢失右值属性

🌏新的类功能

🌲新增的两个默认成员函数

在类和对象的博客中,已经介绍了类的6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

在C++11中由新增了两个默认成员函数:

  • 移动构造函数
  • 移动赋值运算符重载

这两个函数相信大家都不陌生,上面介绍右值引用中也介绍了这两个函数,右值引用和这两个函数结合使用才能够彰显出右值引用的实际意义。需要注意的几点是:

  • 如果没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造
  • 默认生成的移动构造函数,对于内置类型成员会按照字节序进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
  • 如果没有实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值
  • 默认生成的移动构造函数,对于内置类型成员会按照字节序进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值

可以看出,想让编译器自动生成移动构造和移动赋值要求还是很严格的。

实例演示: 为了方便观察,这里我使用自己简单模拟实现的string来进行演示。拿析构函数做演示,有析构函数和没有析构函数,两种情况下,使用右值对象构造一个对象和使用右值对象给一个对象赋值,观察会调用哪个函数

namespace Simulation

	class string
	
	public:
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1]<

以上是关于C++进阶第二十五篇——C++11(列表初始化+变量类型推导+右值引用和移动语义+新的类功能+可变模板参数)的主要内容,如果未能解决你的问题,请参考以下文章

C++进阶第二十五篇——C++11(列表初始化+变量类型推导+右值引用和移动语义+新的类功能+可变模板参数)

C++从青铜到王者第二十五篇:C++智能指针

STM32F429第二十五篇之MCU屏实验详解

STM32F429第二十五篇之MCU屏实验详解

Django 第二十五篇Django admin源码解析

C++进阶第十五篇—C++中的继承(继承的概念+菱形继承+虚拟继承+组合)