详解C++11

文章目录

  • 前言
  • 一、C++11简介
  • 二、统一的列表初始化
    • 2.1 {}的初始化
    • 2.2 std::initializer_list
  • 三、声明
    • 3.1 auto
    • 3.2 decltype
    • 3.3 nullptr
  • 四、范围for
  • 五、智能指针
  • 六、STL中一些变化
    • 新容器
    • 容器中的一些新方法
  • 七、右值引用和移动语义
    • 7.1 左值引用和右值引用
    • 7.2 左值引用和右值引用比较
      • 左值引用总结
      • 右值引用总结
    • 7.3 右值引用使用场景和意义
    • 7.4 右值引用引用左值及其一些更深入的使用场景分析
    • 7.5 完美转发
  • 八、新的类功能
    • 类成员变量初始化
    • 强制生成默认函数的关键字default:
    • 禁止生成默认函数的关键字delete:
    • 继承和多态中的final和override关键字
  • 九、可变参数模板
    • 递归函数方式展开参数包
    • 逗号表达式展开参数包
    • STL容器中的emplace相关接口函数:
  • 十、lambda表达式
    • 10.1 C++98中的一个例子
    • 10.2 lambda表达式
    • 10.3 lambda表达式语法
    • 10.3.1 lambda表达式各部分说明
      • 10.3.2 捕获列表说明
    • 10.4 函数对象与lambda表达式
  • 十一、包装器
    • function包装器
    • bind
  • 总结


前言

每日一句:请不要相信,胜利就像山坡上的蒲公英一样唾手可得。但是请相信,世上总有一些美好值得我们全力以赴,哪怕粉身碎骨!


正文开始!

一、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++11增加的语法特性非常篇幅非常多,我这里没办法一一讲解,所以本节课程主要讲解实际中比较实用的语法。
C++11的官方文档介绍
小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。

二、统一的列表初始化

2.1 {}的初始化

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

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };
	return 0;
}

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

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int x1 = 1;//建议就用这个,下面的就不要用了
	int x2{ 2 };
	int x3 = { 3 };
	int array1[]{ 1, 2, 3, 4, 5 };
	int array2[5]{ 0 };
	Point p{ 1, 2 };
	//上面支持本质就更好的支持了new[]的初始化问题
	// C++11中列表初始化也可以适用于new表达式中
	int* p1=new int(1);
	int* p2 = new int[4] { 1,2,3,4 };
	return 0;
}

详解C++11_第1张图片
创建对象时也可以使用列表初始化方式调用构造函数初始化!

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 1, 1); // old style
	// C++11支持的列表初始化,这里会调用构造函数初始化
	Date d2{ 2022, 1, 2 };
	Date d3 = { 2022, 1, 3 };
	Date* p1 = new Date(2022, 1, 4);
	Date* p2 = new Date[2]{ {2022,1,5},{2022,1,6} };
	return 0;
}

详解C++11_第2张图片
对容器的初始化

int main()
{
	vector<int> v1 = { 1,2,3,4,5 };
	vector<int> v2{6,7,8,9,10};
	vector<Date> v3 = { {2022,1,1},{2022,1,2},{2022,1,3} };
	set<int> s1{1,2,3};
	map<string, string> dict = { {"string","字符串"},{"sort","排序"},{"left","左边"}};

	return 0;
}

2.2 std::initializer_list

std::initializer_list的文档介绍

std::initializer_list是什么类型:

int main()
{
	//the type of il is an initializer_list
	auto il = {1,2,3};
	cout << typeid(il).name() << endl;
	initializer_list<double> ild = { 1.1,2.2,3.3 };
	initializer_list<double>::iterator it =ild.begin();
	while (it != ild.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	for (auto& e : ild)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

详解C++11_第3张图片
对容器的初始化底层也是通过先对initializer_list的初始化再去初始化容器
详解C++11_第4张图片
详解C++11_第5张图片
std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。

三、声明

C++11提供了多种简化声明的方式,尤其是在使用模板时。

3.1 auto

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

int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcpy;
	cout << typeid(p).name() << endl;
	cout << typeid(pf).name() << endl;
	map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
	//map::iterator it = dict.begin();
	auto it = dict.begin();
	cout << typeid(it).name() << endl;
	return 0;
}

3.2 decltype

关键字decltype将变量的类型声明为表达式指定的类型。

//decltype的一些使用场景
template<class T1,class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}

int main()
{
	const int x = 1;
	double y = 2.2;

	decltype(x*y) ret;	// ret的类型是double
	decltype(&x) p;	// p的类型是int const *
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;

	F(1, 'a');

	return 0;
}

详解C++11_第6张图片

3.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能会带来一些问题,因为0既能表示指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

四、范围for

这个我在前面的章节中已经进行非常详细的讲解,这里就不讲解了。

五、智能指针

因为这个非常的重要,下篇博客单独去讲解智能指针!

六、STL中一些变化

新容器

用橘色圈起来的是C++11中的一些新容器,但是实际最有用的是unordered_set和unordered_map,这两个我前面已经进行了非常详细的讲解(哈希----unordered_xxx的使用和模拟实现),其他的容器大家简单了解一下即可!

详解C++11_第7张图片

容器中的一些新方法

如果仔细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用的比较少的!

比如提供了cbegin和cend方法返回const迭代器等等,但是实际的意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上C++11更新之后,容器中增加的新方法最后用的插入接口函数的右值引用版本:
http://www.cplusplus.com/reference/vector/vector/emplace_back/
http://www.cplusplus.com/reference/vector/vector/push_back/
http://www.cplusplus.com/reference/map/map/insert/
http://www.cplusplus.com/reference/map/map/emplace/

但是这些接口的意义到底在哪?网上都说它们能提高效率,他们是如何提高效率的?

稍后我来带大家去了解!

七、右值引用和移动语义

7.1 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用的语法特性,所以从现在开始我们之前学习的就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名!

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量,表达式返回值,函数返回值(这个不能是左值引用返回)等等,他们都是临时变量右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名!

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);

	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	/*10 = 1;
	x + y = 1;
	fmin(x, y) = 1;*/
	return 0;
}

左值和右值最大的区别就是:左值可以取地址,右值不能取地址!

在给右值取别名后,会导致右值被存储到特定位置,切可以取到该位置的地址,也就是说例如:不能去字面量10的地址,但是rr1引用以后,可以对rr1取地址,也可以修改rr1.如果不想rr1被修改,可以用const int&& rr1去引用,是不是感觉很神奇呢…

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	//rr2 = 5.5;  // 报错
	return 0;
}

7.2 左值引用和右值引用比较

左值引用总结

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可以引用左值,可以引用右值。
int main()
{
	//左值引用只能引用左值,不能引用右值
	int a = 10;
	int& ra1 = a;	//ra为a的别名
	//int& ra2 = 10;	//编译失败,因为10是右值

	// const左值引用既可以引用左值,也可以引用右值
	const int& ra3 = 10;
	const int& ra4 = a;

	return 0;
}

右值引用总结

  1. 右值引用只能引用右值,不能引用左值。
  2. 但是右值引用可以引用move以后的左值。
int main()
{
	//右值引用只能引用右值,不能引用左值
	int&& r1 = 10;

	// error C2440:"初始化":无法从"int"转化为"int&&"
	// message:无法将左值绑定到右值引用
	int a = 10;
	//int&& r2 = a;//error

	//右值引用可以引用move以后的左值
	int&& r3 = move(a);
	return 0;
}

7.3 右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值也可以引用右值,那么为什么C++11还要提出右值引用呢?是不是有点画蛇添足了呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

namespace hulu
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 资源转移" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 资源转移" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
	// 下面的场景面临的问题,只能使用传值返回,左值引用解决不了
	hulu::string operator+(const hulu::string& s, char ch)
	{
		hulu::string ret(s);
		ret += ch;
		return ret;
	}

	//hulu::string to_string(int value)
	hulu::string& to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		hulu::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}

//void func1(hulu::string s)
void func1(const hulu::string& s)	//左值引用解决
{
}
string operator+=(char ch)
//string& operator+=(char ch)	//左值引用解决
//{
//	push_back(ch);
//	return *this;
//}



int main()
{
	/*hulu::string s1("hello");
	func1(s1);*/
	
	// error 程序崩溃了!--->str除了作用域就会调用它的析构函数
	/*hulu::string ret = hulu::to_string(1234);
	cout << ret.c_str() << endl;*/

	hulu::string s1("hello");

	hulu::string s2(s1);	//深拷贝
	hulu::string s3(move(s1));	//转移将亡值的资源

	return 0;
}

详解C++11_第8张图片

详解C++11_第9张图片
右值引用和移动语义解决上述问题:
在hulu::string中增加了移动构造,移动构造的本质是将参数右值的资源窃取过来,占为己有,那么就不用再做深拷贝饿了,所以它叫做移动构造,就是窃取别人的资源来构造自己!
详解C++11_第10张图片
STL的容器,C++11以后,都提供了移动构造和移动赋值。

http://www.cplusplus.com/reference/string/string/string/

http://www.cplusplus.com/reference/vector/vector/vector/

左值引用的深拷贝 – 拷贝构造/拷贝赋值

右值引用的深拷贝 – 移动构造/移动赋值

  1. 深拷贝对象,传值返回,调用移动构造,那么效率就提高了!

7.4 右值引用引用左值及其一些更深入的使用场景分析

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

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
 return ((typename remove_reference<_Ty>::type&&)_Arg);
}
int main()
{
	list<hulu::string> lt;
	hulu::string s1("1111");
	// 这里调用的是拷贝构造
	lt.push_back(s1);	//--> void push_back(const value_type & val);
	// 下面调用都是移动构造
	lt.push_back("2222");	//	--> void push_back (value_type&& val);
	lt.push_back(std::move(s1));//	--> void push_back (value_type&& val);
	return 0;
}

详解C++11_第11张图片
STL容器插入接口函数也增加了右值引用的版本:

http://www.cplusplus.com/reference/list/list/push_back/
http://www.cplusplus.com/reference/vector/vector/push_back/

7.5 完美转发

模板中的&& 万能引用

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; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)//可以理解为 int&& r1=10;  r1就是一个左值,就类比这里的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;
}

详解C++11_第12张图片
std::forward 完美转发在传参过程中保留了对象原生的类型属性

详解C++11_第13张图片

完美转发实际中的使用场景:

template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};
template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void PushBack(T&& x)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));
	}
	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};
int main()
{
	List<hulu::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}

在这里插入图片描述

八、新的类功能

默认成员函数
原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const取地址重载
    最重要的是前四个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员汇之星逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值,默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果是实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值更上面得移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

不写析构函数、拷贝构造、拷贝赋值重载
就会调用默认生成移动构造和移动赋值

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	/*Person(const Person& p)
	* :_name(p._name)
	 ,_age(p._age)
	 {}*/
	 /*Person& operator=(const Person& p)
	 { 
	 if(this != &p)
	 {
	 _name = p._name;
	 _age = p._age;
	 }
	 return *this;
	 }*/
	 /*~Person()
	 {}*/
private:
	hulu::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

详解C++11_第14张图片
写析构函数、拷贝构造、拷贝赋值重载
详解C++11_第15张图片

类成员变量初始化

C++11允许在类定义是给成员变量初始缺省值,默认生成构造函数回事呀这些缺省值初始化,这个我在类和对象就已经讲过了,这里就不细讲了!

强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显式指定移动构造生成。

详解C++11_第16张图片
详解C++11_第17张图片

禁止生成默认函数的关键字delete:

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

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p) = delete;
private:
	hulu::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

详解C++11_第18张图片

继承和多态中的final和override关键字

这个我在继承和多态章节已经进行了详细的讲解,这里就不做讲解了!

九、可变参数模板

C++11的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板,相比于C++98/03,类模板和函数模板只能含固定数量的模板参数,可变模板参数无疑是一个巨大的改进。然后由于可变模板参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后有需要的话,可以在深入学习。

下面就是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template<class ...Args>
void ShowList1(Args... args)
{ 
	//参数个数
	cout << sizeof...(args) << endl;
 }
template<class ...X>
void ShowList2(X... y)
{ }
int main()
{
	ShowList1(1, 'x', 1.1);
	ShowList1(1, 2,3,4);
	return 0;
}

详解C++11_第19张图片
上面的参数args前面有省略号,所以他就是一个可变模板参数,我们把带省略号的参数称为"参数包",它里面包含了0到N(N>=0)个模板参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模板参数的一个主要特点,也是最大的难点,即如何展开可变模板参数。由于语法不支持使用args[i]这样的方式获取可变参数,所以我们得用一些奇招来一一获取参数包的值。

递归函数方式展开参数包

template<class T>
void ShowList(const T& val)
{
	cout << val << typeid(val).name() << endl;
}
//void ShowList()
//{}
//上面两个都可以使用
template<class T,class ...Args>
void ShowList(const T& val,Args... args)
{
	cout << val << " "<<typeid(val).name() << endl;
	ShowList(args...);
}

int main()
{
	ShowList(1, 'x', 1.1);
	ShowList(1, 2, 3, 4);
	return 0;
}

详解C++11_第20张图片

逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的,PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(PrintArg(args),0)…,也是按照这个执行顺序,先执行PrintArg(args),在得到逗号表达式的结果为0.同时还用到了C++11的另一个特性—初始化列表,通过初始化列表来初始化一个变长的数组,{(PrintArg(args),0)…}将会展开成(PrintArg(args1),0),(PrintArg(args2),0),etc…,最终会创建一个元素值都为0的数组int arr[sizeof…(args)].由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分PrintArg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程中展开参数包.

//template 
//void PrintArg(T t)
//{
//	cout << t << endl;
//}
template <class T>
int PrintArg(T t)
{
	cout << t << endl;
	return 0;
}
template<class ...Args>
void ShowList( Args... args)
{
	//列表初始化,逗号表达式
	//int arr[] = { (PrintArg(args),0)... };
	int arr[] = { PrintArg(args)...};
	cout << endl;
}

int main()
{
	ShowList(1, 'x', 1.1);
	ShowList(1, 2, 3, 4);
	return 0;
}

详解C++11_第21张图片

STL容器中的emplace相关接口函数:

http://www.cplusplus.com/reference/vector/vector/emplace_back/
http://www.cplusplus.com/reference/list/list/emplace_back/

template <class... Args>
void emplace_back(Args&&... args);

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用,那么相对insert和emplace系列接口的有事到底在哪里呢?

int main()
{
	std::list< std::pair<int, char> > mylist;
	// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
	// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	mylist.emplace_back(make_pair(30, 'c'));
	mylist.push_back(make_pair(40, 'd'));
	mylist.push_back({ 50, 'e' });
	for (auto e : mylist)
		cout << e.first << ":" << e.second << endl;
	return 0;
}
int main()
{
	// 下面我们试一下带有拷贝构造和移动构造的hulu::string,再试试呢
	// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
	// 是先构造,再移动构造,其实也还好。
	std::list< std::pair<int, hulu::string> > mylist;
	mylist.emplace_back(10, "sort");
	mylist.emplace_back(make_pair(20, "sort"));
	cout << endl;
	mylist.push_back(make_pair(30, "sort"));
	mylist.push_back({ 40, "sort" });
	return 0;
}

详解C++11_第22张图片

十、lambda表达式

10.1 C++98中的一个例子

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

#include 
#include 
int main()
{
	int array[] = { 4,1,8,5,3,7,0,9,2,6 };
	// 默认按照小于比较,排出来结果是升序
	std::sort(array, array + sizeof(array) / sizeof(array[0]));
	for (auto& e : array)
	{
		cout << e << " ";
	}
	cout << endl;

	// 如果需要降序,需要改变元素的比较规则
	std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
	for (auto& e : array)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

详解C++11_第23张图片
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

struct Goods
{
	string _name;	//名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};
struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
   3 }, { "菠萝", 1.5, 4 } };
	std::sort(v.begin(), v.end(), ComparePriceLess());
	std::sort(v.begin(), v.end(), ComparePriceGreater());
}

详解C++11_第24张图片
详解C++11_第25张图片
详解C++11_第26张图片
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了lamdba表达式。

10.2 lambda表达式

struct Goods
{
	string _name;	//名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
   3 }, { "菠萝", 1.5, 4 } };

	//按商品的价格升序或者降序排序
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; });
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; });
	
	//按商品的名称升序或者降序排序
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._name > g2._name; });
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._name < g2._name; });
	
	//按商品的评价升序或者降序排序
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate < g2._evaluate; });
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate > g2._evaluate; });

}

上述代码就是使用C++11中的lamdba表达式来解决,可以看出lambda表达式实际是一个匿名函数。

10.3 lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

10.3.1 lambda表达式各部分说明

  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来
    判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda
    函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以
    连同()一起省略
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量
    性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回
    值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
    导。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
    到的变量。

注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中**最简单的lambda函数为:[]{};**该lambda不能做任何事情!

int main()
{
	int a = 10;
	int b = 20;
	//[capture-list] (parameters) mutable -> return-type { statement }
	//捕捉列表 参数 返回值 函数体
	//一般是一个局部的匿名函数,也可以写到全局
	auto add1 = [](int x, int y)->int { return x + y; };
	auto add2 = [](int x, int y){ return x + y; };
	cout << add1(a, b) << endl;
	cout << add2(a, b) << endl;

	return 0;
}

详解C++11_第27张图片
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其辅助给一个变量。

10.3.2 捕获列表说明

捕获列表描述了上下文中那些数据可以被lambda使用,以及使用的方式是传值还是传引用

  • [var]:表示值传递的方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(this)
  • [=]:表示值传递捕捉所有父作用域中的变量(this)
  • [this]:表示值传递方式捕获当前this指针

注意:

  1. 父作用域只包含lambda函数的语句块
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:[=,&a,&b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有的变量
    [&,a,this]:以值传递的方式捕捉变量a和this,引用方式捕捉其他变量
  3. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
    比如:[=,a]:=已经以值传递方式捕捉了所有的变量,捕捉a重复
  4. 在块作用域以外的lambda函数捕捉列表必须为空。
  5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非作用域或者非局部变量都会导致编译报错。
  6. lambda表达式之间不能互相赋值,即使看起来类型相同。
void (*PF)();
int main()
{
	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };
	// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
	//f1 = f2;   // 编译失败--->提示找不到operator=()
	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();
	// 可以将lambda表达式赋值给相同类型的函数指针
	//了解一下,一般不建议这样使用
	PF = f2;
	PF();
	return 0;
}

详解C++11_第28张图片

10.4 函数对象与lambda表达式

函数对象,又称仿函数,既可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。

class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};
int main()
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);
	// lamber
	auto r2 = [=](double monty, int year)->double {return monty * rate * year;
	};
	r2(10000, 2);
	return 0;
}

详解C++11_第29张图片

UUID的简介

每个lambda都会被转换成一个仿函数类型
仿函数类名称lambda+uuid

十一、包装器

function包装器

function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么我们来看看,我们为什么需要function呢?

ret=func(x);
//上面func可能是什么呢?func可能是函数名,函数指针或者是函数对象(仿函数对象)?
//也有可能是lambda表达式对象?所以这些都是可调用的类型!
//如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看。
template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

详解C++11_第30张图片
通过上面的程序验证,我们会发现useF函数模板实例化了三份。

包装器可以很好的解决上面的问题。

// 使用方法如下:
#include 
int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};
class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;
	// 函数对象
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;
	// lamber表达式
	std::function<int(int, int)> func3 = [](const int a, const int b)
	{return a + b; };
	cout << func3(1, 2) << endl;

	// 类的成员函数
	std::function<int(int, int)> func4 = &Plus::plusi;
	cout << func4(1, 2) << endl;

	//非静态成员函数有隐含的this指针,需要多传一个参数,并且需要取地址
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl;
	return 0;
}

详解C++11_第31张图片
有了包装器,如何解决模板的效率低下,实例化多份的问题呢?

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	// lamber表达式
	std::function<double(double)> func3 = [](double d)->double { return d /4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}

详解C++11_第32张图片

bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来"适应"原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没有什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

int f(int a, int b)
{
	return a - b;
}
class Plus
{
public:
	Plus(int x=2)
		:_x(x)
	{}
	int plusi(int a, int b)
	{
		return (a + b)*_x;
	}
private:
	int _x;
};
int main()
{
	// 调整顺序
	std::function<int(int, int)> func = bind(f,placeholders::_2,placeholders::_1);
	cout << func(1, 2) << endl;

	// 调整可调用对象的参数个数和顺序
	//_1,_2....表示你要自己穿的那些参数
	// 调整个数为两个参数
	std::function<int(int, int)> func1 = bind(&Plus::plusi,Plus(10),placeholders::_1,placeholders::_2);
	cout << func1(1, 2) << endl;

	// 调整个数为一个参数
	std::function<int(int)> func2 = bind(&Plus::plusi, Plus(10), 5.5, placeholders::_1);
	cout << func2(2) << endl;
	return 0;
}

详解C++11_第33张图片


总结

C++11中的线程库我之后给大家总结在一起单独出一篇博客讲解,大家拭目以待啦!!!
至此C++11的重要内容完结!
(本章完!)

你可能感兴趣的:(c++,算法,数据结构)