c++从青铜到王者第二十二篇:c++11(代码片段)

森明帮大于黑虎帮 森明帮大于黑虎帮     2022-12-28     232

关键词:

系列文章目录



前言


一、C++11简介(了解)

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

二、 列表初始化(了解)

1.C++98中的初始化问题

在C++98中,标准允许使用花括号对数组元素进行统一的列表初始值设定。比如:

int array1[] = 1,2,3,4,5;
int array2[5] = 0;

对于一些自定义的类型,却无法使用这样的初始化。比如:

vector v1,2,3,4,5;

就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

2.内置类型的列表初始化

int main()

	// 内置类型变量
	int x1 =  10 ;
	int x2 10 ;
	int x3 = 1 + 2;
	int x4 =  1 + 2 ;
	int x5 1 + 2 ;
	// 数组
	int arr1[5] 1, 2, 3, 4, 5;
	int arr2[]1, 2, 3, 4, 5;
	// 动态数组,在C++98中不支持
	int* arr3 = new int[5]1, 2, 3, 4, 5;
	// 标准容器
	vector<int> v 1, 2, 3, 4, 5 ;
	map<int, int> m  1, 1 ,  2, 2, ,  3, 3 ,  4, 4  ;
	return 0;

注意:列表初始化可以在之前使用等号,其效果与不使用=没有什么区别。

3.自定义类型的列表初始化

标准库支持单个对象的列表初始化。

class Point

public:
	Point(int x = 0, int y = 0) : _x(x), _y(y)
	
private:
	int _x;
	int _y;
;
int main()

	Pointer p 1, 2 ;
	return 0;

多个对象的列表初始化:

多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。

#include <initializer_list>
template<class T>
class Vector 
public:
	// ...
	Vector(initializer_list<T> l) : _capacity(l.size()), _size(0)
	
		_array = new T[_capacity];
		for (auto e : l)
			_array[_size++] = e;
	
	Vector<T>& operator=(initializer_list<T> l) 
		delete[] _array;
		size_t i = 0;
		for (auto e : l)
			_array[i++] = e;
		return *this;
	
	// ...
private:
	T* _array;
	size_t _capacity;
	size_t _size;
;

三、变量类型推导(了解)

1.为什么需要类型推导


在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如:

#include <map>
#include <string>
int main()

	short a = 32670;
	short b = 32670;
	// c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
	short c = a + b;
	std::map<std::string, std::string> m  "apple", "苹果" ,  "banana", "香蕉"  ;
	// 使用迭代器遍历容器, 迭代器类型太繁琐
	std::map<std::string, std::string>::iterator it = m.begin();
	while (it != m.end())
	
		cout << it->first << " " << it->second << endl;
		++it;
	
	return 0;


C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。关于auto的详细介绍可以参考前面的博客。

2. decltype类型推导

1.为什么需要decltype

auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。

template<class T1, class T2>
T1 Add(const T1& left, const T2& right)

   	return left + right;

如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。

C++98中确实已经支持RTTI:

  • typeid只能查看类型名字不能用其结果类定义类型。
  • dynamic_cast只能应用于含有虚函数的继承体系中。

运行时类型识别的缺陷是降低程序运行的效率。

2. decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如:

  • 推演表达式类型作为变量的定义类型。
int main()

	int a = 10;
	int b = 20;
	// 用decltype推演a+b的实际类型,作为定义c的类型
	decltype(a + b) c;
	cout << typeid(c).name() << endl;
	return 0;

  • 推演函数返回值的类型:
void* GetMemory(size_t size)

	return malloc(size);

int main()

	// 如果没有带参数,推导函数的类型
	cout << typeid(decltype(GetMemory)).name() << endl;
	// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
	cout << typeid(decltype(GetMemory(0))).name() << endl;
	return 0;

四、范围for循环

参考前面博客:C++入门

五、final与override

六、智能指针

后续文章讲解。

七、新增加容器—静态数组array、forward_list以及unordered系列

八、默认成员函数控制(了解)

在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。

1.显式缺省函数


在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。

class A

public:
	A(int a) : _a(a)
	
	// 显式缺省构造函数,由编译器生成,一定生成
	A() = default;
	// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
	A& operator=(const A& a);
private:
	int _a;
;
A& A::operator=(const A& a) = default;
int main()

	A a1(10);
	A a2;
	a2 = a1;
	return 0;

2.删除默认函数

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class A

public:
	A(int a) : _a(a)
	
	// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
	A& operator(const A&) = delete;
private:
	int _a;
;
int main()

	A a1(10);
	// 编译失败,因为该类没有拷贝构造函数
	//A a2(a1);
	// 编译失败,因为该类没有赋值运算符重载
	A a3(20);
	a3 = a2;
	return 0;

九、右值引用(熟悉)

1.右值引用概念

C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;

void Swap(int& left, int & right)

	int tmp = left;
	left = right;
	right = tmp;

为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。

int add(int a, int b)

	return a + b;

int main()

	int a = 10;
	int b = 20;
	Swap(a, b);

	const int&& ra = 10;

	// 引用函数返回值,返回值是一个临时变量,为右值
	int&& rRet = add(10, 20);

	return 0;

为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。

2.左值与右值


左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
using namespace std;

int main()

	int x = 1;
	int y = 2;

	//左值引用的定义
	int a = 0;
	int &b = a;

	//左值引用不能引用右值,但const左值引用可以
	//int& e = 10;
	//int& f = x + y;
	const int& e = 10;
	const int& f = x + y;

	//右值引用的定义
	int&& c = 10;
	int&& d = x + y;

	//右值引用不能引用左值,但是可以引用move后左值
	//int&& m = a;
	int&& m = move(a);
	
	return 0;

  • 因此关于左值与右值的区分不是很好区分,一般认为:
  • 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  • const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
  • 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  • 如果表达式运行结果或单个变量是一个引用则认为是左值。
  • 总结:
  • 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量。
  • 能得到引用的表达式一定能够作为引用,否则就用常引用。
  • C++11对右值进行了严格的区分::
  • C语言中的纯右值,比如:a+b, 100。
  • 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。

3.引用与右值引用比较

在C++98中的普通引用与const引用在引用实体上的区别:

int main()

	// 普通类型引用只能引用左值,不能引用右值
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;

注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。

C++11中右值引用:只能引用右值,一般情况不能直接引用左值。

int main()

	// 10纯右值,本来只是一个符号,没有具体的空间,
	// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
	int&& r1 = 10;
	r1 = 100;
	int a = 10;
	int&& r2 = a; // 编译失败:右值引用不能引用左值
	return 0;

问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?

4.值的形式返回对象的缺陷

如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:

#define _CRT_SECURE_NO_WARNINGS   1
#include<iostream>
#include<string>

namespace yyw

	class string
	
		string(const char* str = "")
		:_str(new char[strlen(str)+1])
		
			strcpy(_str, str);
		

		//s2(s1)
		string(const string& s)
			:_str(new char[strlen(s._str)+1])
		
			strcpy(_str, s._str);
		

		//s2=s1
		string& operator=(const string& s)
		
			if (this != &s)
			
				char* _Pstr = new char[strlen(s._str) + 1];
				strcpy(_Pstr, s._str);
				delete[] _str;
				_str = _Pstr;
			
			return *this;
		
		~string()
		
			delete[]_str;
			_str=nullptr;
		
	private:
		char* _str;
	;

上述代码看起来没有什么问题,但是有一个不太尽人意的地方:

在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。仔细观察会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢。

5.移动语义

C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。

在C++11中如果需要实现移动语义,必须使用右值引用。上述String类增加移动构造:

因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。

注意:

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

6.右值引用引用左值

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

int main()

	String s1("hello world");
	String s2(move(s1));
	String s3(s2);
	return 0;

注意:以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串。
查看详情

c++从青铜到王者第二十三篇:c++异常(代码片段)

系列文章目录文章目录系列文章目录前言一、C语言传统的处理错误的方式二、C++异常概念三、异常的使用1.异常的抛出和捕获2.异常的重新抛出3.异常安全4.异常规范四、自定义异常体系五、C++标准库的异常体系六、... 查看详情

c++从青铜到王者第二十四篇:c++的类型转换(代码片段)

系列文章目录文章目录系列文章目录前言一、C语言中的类型转换二、为什么C++需要四种类型转换三、C++强制类型转换1.static_cast类型转换2.reinterpret_cast类型转换3.const_cast类型转换4.dynamic_cast类型转换5.explicit总结前言一... 查看详情

c++从青铜到王者第十二篇:深入理解默认成员函数之日期类的实现(代码片段)

系列文章目录文章目录系列文章目录前言一、类的6个默认成员函数二、日期类的实现总结前言一、类的6个默认成员函数二、日期类的实现#define_CRT_SECURE_NO_WARNINGS1#include<iostream>usingnamespacestd;classDatepublic: //获取某年某月的天... 查看详情

c++从青铜到王者第二十六篇:哈希(代码片段)

系列文章目录文章目录系列文章目录前言一、unordered系列关联式容器二、unordered_map1.unordered_map的文档介绍2unordered_map的接口介绍三、unordered_set四、底层结构1.哈希概念2.哈希冲突3.哈希函数4.哈希冲突的解决之闭散列-线性探测和... 查看详情

c++从青铜到王者第二十五篇:c++智能指针(代码片段)

系列文章目录文章目录系列文章目录前言一、常见面试题1.malloc/free和new/delete的区别2.内存泄漏1.内存泄漏概念与危害2.内存泄漏分类(了解)3.如何检测内存泄漏(了解)4.如何避免内存泄漏5.如何一次在堆上申请4G... 查看详情

c++从青铜到王者第二十七篇:特殊类设计(代码片段)

系列文章目录文章目录系列文章目录前言一、请设计一个类,只能在堆上创建对象二、请设计一个类,只能在栈上创建对象三、请设计一个类,不能被拷贝四、请设计一个类,不能被继承五、请设计一个类,... 查看详情

c++从青铜到王者第二十篇:stl之setmapmultisetmultimap的初识(代码片段)

系列文章目录文章目录系列文章目录前言一、关联式容器二、键值对三、树形结构的关联式容器四、set的介绍和使用1.set的介绍2.set的使用1.set的模板参数列表2.set的构造3.set的容量4.set的修改操作5.set的迭代器五、map的介绍和使用1... 查看详情

c++从青铜到王者第二十一篇:哈希的应用之位图布隆过滤器(代码片段)

系列文章目录文章目录系列文章目录前言一、位图1.位图的概念2.位图的面试题3.位图的实现4.位图的应用二、布隆过滤器1.布隆过滤器的提出2.布隆过滤器的概念3.布隆过滤器的插入3.布隆过滤器的查找4.布隆过滤器的删除5.布隆过... 查看详情

c++从入门到入土第二十二篇:数据结构之红黑树(代码片段)

红黑树文章目录红黑树一、红黑树简介性质节点红黑树的实现一、红黑树红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。... 查看详情

c++从入门到入土第二十二篇:数据结构之红黑树(代码片段)

红黑树文章目录红黑树一、红黑树简介性质节点红黑树的实现一、红黑树红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。... 查看详情

git从青铜到王者第二篇:git的初始(代码片段)

系列文章目录文章目录系列文章目录前言一、Git关于版本控制1.本地版本控制系统2.集中化的版本控制系统3.分布式版本控制系统二、Git简史三、Git是什么1.Git是什么2.直接记录快照,而非差异比较3.近乎所有操作都是本地执行4... 查看详情

love2d从青铜到王者第十二篇:love2d之碰撞检测(detectingcollision)(代码片段)

系列文章目录文章目录系列文章目录前言🍇一、图像(Images)1️⃣.图像(Images)🍈二、总结🍋总结前言🍇一、图像(Images)1️⃣.图像(Images)假设我们正在做一个游戏,在... 查看详情

lua从青铜到王者基础篇第二篇:lua基本语法和数据类型(代码片段)

系列文章目录文章目录系列文章目录前言🌲一、Lua基本语法1.第一个Lua程序2.交互式编程3.脚本式编程4.注释1.单行注释2.多行注释1.多行注释注意事项5.标识符6.关键词7.全局变量🌳二、Lua数据类型1.nil(空)2.boolean(布尔&... 查看详情

linux从青铜到王者第十六篇:linux网络基础第二篇之http协议(代码片段)

系列文章目录文章目录系列文章目录前言一、HTTP协议的概念二、HTTP协议URL的解释三、HTTP协议的数据流四、HTTP协议格式1.HTTP请求2.HTTP响应五、HTTP协议格式图解六、HTTP协议版本七、HTTP协议请求方法1.GET:获取资源2.POST:... 查看详情

linux从青铜到王者第二十四篇:linux网络基础第四篇之websocket协议(代码片段)

系列文章目录文章目录系列文章目录前言一、WebSocket简介二、WebSocket产生背景三、WebSocket实现原理四、WebSocket协议举例五、WebSocket使用1.WebSocket介绍2.WebSocketAPI3.WebSocket事件1.open2.Message3.Error4.Close4.WebSocket方法1.send()2.close()5.WebSocket... 查看详情

c++从青铜到王者第十八篇:c++之多态(代码片段)

系列文章目录文章目录系列文章目录前言一、多态的概念1.多态的概念二、多态的定义及实现1.多态的构成条件2.虚函数的认识3.虚函数的重写1.虚函数重写的两个例外之协变2.虚函数重写的两个例外之析构函数的重写4.C++11ove... 查看详情

设计模式从青铜到王者第二篇:uml类图与面向对象编程

系列文章目录文章目录系列文章目录前言一、设计概念二、对象和类三、类层次结构四、面向对象程序设计基础概念1.抽象2.封装3.继承4.多态5.对象之间的关系总结前言一、设计概念面向对象程序设计(Object-OrientedProgramming... 查看详情