C++ 11

文章目录


前言: C++11 较C++98 更新许多有用的库函数,以及一些新的特性,使得C++能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。本文,主要讲解C++ 11 相较C++ 98 做出的一些更新。


1. 列表初始化

为什么要有列表初始化呢?它的出现 使得 初始化 自定义对象时,更加的方便高效。

  • C++98 中 什么是支持列表初始化的?数组。
  • C++11 中 什么是支持列表初始化的 ?所有的内置类型,以及用户自定义的类型。

举个例子:

在C++98下,在vector容器中插入值,需要一个一个的push_back()。

// C++ 98
	int a[] = { 1,2,3,4,5 };

	vector<int> aa;

	for (int i =1 ;i<=5;i++)
	{
		aa.push_back(i);
	}
	

但C++11下,支持了 列表初始化:

vector<int> a1 = { 1,2,3,4,5 };

这样初始化高效了许多,当然还支持很多容器 去利用 列表初始化:

    vector<int> a1 = { 1,2,3,4,5 };

	list<int> l1 = { 1,2,3,4,5 };

	map<int, string> m1 = { {1,"hh"},{2,"ww"},{3,"ll"} };

	string s1 = { "wwwww" };

自定义对象,也是可以利用列表初始化的:

class A
{
private:
	int _a;
	int _b;
public:
	A(int a,int b)
		:_a(a),
		_b(b)
	{}
};

int main()
{
  A _a = {1,2};
}

1.1 列表初始化的使用格式

上面 只是 展示一下 列表初始化的使用,接下来 我们 来具体的说明一下。

上面 都是 用了 加 = 的格式,其实也可以不用加 =

1.1.1 内置类型
   // 内置类型
	int a1{ 3 };
	char s1{ 'w' };

	// 数组
	int a[]{ 1,2,3,4,5 };
	char s[]{ "ssssss" };
	
	// 动态数组

	int* p1 = new int[]{1, 2, 3, 4, 5};
	char* p2 = new char[] {"ssssss"};

	// 标准容器

	vector<int> v{ 1,2,3,4,5 };
	map<int, string> m{ {1,"hh"},{2,"ww"} };
1.1.2 自定义类型的列表初始化
class A
{
private:
	int _a;
	int _b;
public:
	A(int a,int b)
		:_a(a),
		_b(b)
	{
	}
};

int main()
{
  A _a{1,2};
}

1.2 列表初始化的本质

支持列表初始化用的是 initialzer_list

在这里插入图片描述
上面也标注了,它是C++11 才开始有的。

它的成员函数:

在这里插入图片描述
也就是说:

{1,2,3,4,5} 这种列表,它是一个 initialzer_list对象。

我举个例子:

   initializer_list<int> il;

	il = { 1,2,3,4,5 };

	cout << il.size() << endl;


	cout << il.begin() << endl;
	cout << *il.begin() << endl;
	cout << il.end() << endl;
	cout << *(il.end()-1) << endl;

size() 是 列表的大小,begin() 指向第一个元素;end() 指向 最后一个元素的下一个位置。

运行结果如下:

在这里插入图片描述


所以说:列表初始化本质是 将列表对象initialzer_list中的值 赋值到 指定对象 里。

它不是传统意义上的 拷贝构造,咱们熟知的拷贝构造是 拷贝同类型的对象,这个比较特殊,拷贝的是initialzer_list里的值。

我们去官方文档中查看一些:

vector重载的构造函数
在这里插入图片描述
string:
在这里插入图片描述
list:
在这里插入图片描述
还有很多,不一 一 展示了。


我们来画图理解一下:

vector<int> v = { 1,2,3,4,5 };

先是形成了 {1,2,3,4,5}的initialzer_list的对象,然后 再去 调用vector重载的拷贝构造:

在这里插入图片描述


但是 有个疑问 :自定义对象中,并没有重载A (initializer_list< value_type > il) ,是怎么实现的 列表拷贝?

发生了隐式转换,即便你没有主动写 列表拷贝构造,类里会有默认生成的 供你使用,默认的构造函数,这大家应该懂。

比如:我使用 关键字 explicit ,修饰构造函数,使得不能发生隐式转换,看看会有什么效果

explicit A(int a,int b)
		     :_a(a),
		     _b(b)
	{}
A a_ = {1,2};

可以看到 直接 报错了,复制列表初始化 …… 不能 ……:

在这里插入图片描述
这就反向的证明了 发生隐式转换 。

2. 变量类型的推导

变量类型推导怎么说呢? 它 是比较方便的,比如 遇到比较复杂的类型 ,自己写起来很不方便,直接利用 类型推导 就可以了。

2.1 auto 关键字

auto 可以 根据后面的值,进行类型推导 :

在这里插入图片描述
当然上面的例子用的很少,关键 是推导那些复杂的类型:

map<int, string> m;

std::map<int, string> ::iterator i =m.begin();

这样的迭代器 比较 复杂吧,看 如果是用 auto呢?

auto i = m.begin();

2.2 decltype类型推导

为什么要有 decltype 推导呢?按理说 有auto 推导就比较方便。

因为auto使用的前提:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型,也就是 说 某些在 编译过程中才初始化的类型,auto 无法进行推导,所以 就有了 decltype类型推导。

不能使用auto的例子:

(1)这个函数调用就明显不可以:

int add(auto x, auto y)
{
	return x + y;
}

在这里插入图片描述
(2)auto当 模板

	vector<auto>s;

在这里插入图片描述
(3) auto数组的初始化

auto i[] = { 1,2,3,4 };

在这里插入图片描述

等等例子,终归到底,使用auto推导,auto声明的类型已经初始化。


然后看 decltype推导的例子:

    int a = 1;
	int b = 2;
	decltype(a+b) c;
	
	cout << typeid(c).name() << endl;

decltype(a+b) 相当于 推导出 a+b的类型,然后用推导出的类型,定义了一个变量 c。然后 利用 typeid (c).name() ,知晓它的类型:

运行结果:
在这里插入图片描述

对吧,其实 变量类型推导 是容易理解的。


3. 范围for

范围for 又被称为 语法糖,因为用起来比较的甜(好用)。

它的底层其实 就是 迭代器的使用:

比如我要 遍历vector ,那么我可以使用下标遍历,也可以用迭代器,也能用 范围for。

   vector<int>vc{ 1,2,3,4,5,6,7,8,9,10 };

	vector<int>::iterator i = vc.begin();

	while (i != vc.end())
	{
		cout << *i << endl;
		i++;
	}

上面使用迭代器版本的,现在我们来使用范围for:

    for (auto& e : vc)
	{
		cout << e << endl;
	}

明显下面的比较简单,看看运行结果:

在这里插入图片描述

  • 范围for广泛用于 遍历容器的操作,它使得遍历 变得简单。
  • 它的本质是利用的迭代器,所以 要求迭代器支持begin(),end(),!=,++等操作。

4. final与override

  • final 放在类后,表示该类不能被继承;放在虚函数后,表示该虚函数不能被重写
  • override 用于检查 派生类虚函数 是否重写了基类的被override修饰的虚函数,如果没有重写就会报错。

举个例子:

class person
{
public:
	virtual void buy_ticekt()
	{
		cout << "买的票,是全价" << endl;
	}
};

class student : public person
{
public:
	virtual void buy_ticekt()
	{
		cout << "买的票,是半价" << endl;
	}
};

这是一个简单的单继承,而且还实现了多态。

  • 先来验证final :
class person final
{
public:
	virtual void buy_ticekt()
	{
		cout << "买的票,是全价" << endl;
	}
};

class student : public person
{
public:
	virtual void buy_ticekt()
	{
		cout << "买的票,是半价" << endl;
	}
};

在这里插入图片描述


class person 
{
public:
	virtual void buy_ticekt() final
	{
		cout << "买的票,是全价" << endl;
	}
};

class student : public person
{
public:
	virtual void buy_ticekt()
	{
		cout << "买的票,是半价" << endl;
	}
};

在这里插入图片描述


  • 再来验证 override:
class person 
{
public:
	virtual void buy_ticekt() 
	{
		cout << "买的票,是全价" << endl;
	}
};

class student : public person
{
public:
	virtual void buy_ticekt(int a =1) override
	{
		cout << "买的票,是半价" << endl;
	}
};

在这里插入图片描述


综上理解一下:

  • final 相当于限制了 某个类不能被继承,或者类中的某个虚函数不能被重写,用于父类中
  • override 相当于 提个醒,提醒子类要对父类的某个虚函数进行重写,没重写会报错,它用于子类中。

5. 智能指针

至于,智能指针,后续会给出文章链接,还没肝完。

6. 新增容器

新增的容器 有array ,forward_list 以及unordered系列。

6.1 静态数组array

在这里插入图片描述
模板参数 T是定义的静态数组的元素类型,N是 元素个数。

比如: array<int, 10> a;就是定义了一个定长的数组,它的元素类型是int,包含10个元素。

有点奇怪,明明我定义 一个静态的数组,其是方法是有的:

#define N 10
int main()
{
  	int arr[N];
}

定义动态数组可以使用vector。结果 又搞出来一个array。

这个确实被人吐槽过,但它存在必然还是有点价值的,比如它提供了些接口函数:

在这里插入图片描述
有迭代器,容量,还支持随机访问等,对吧,其实用的也不多。

简单评价:食之无味,弃之可惜。


6.2 单向链表 forward_list

有来个单向链表forward_list ,本来是用的双向链表 list,为什么又要整出来个单向链表呢?

它怎么说呢?有些时候,它是要比list高效的,

比如:
存储相同个数的同类型元素,单链表耗用的内存空间更少,空间利用率更高,并且对于实现某些操作单链表的执行效率也更高。

但是单向链表 只支持 从前往后 遍历 ,因为单向嘛。它支持头插,头删,也支持任意位置的插入,只不过 插入也有点奇怪。

(1) 构造函数
在这里插入图片描述

int main ()
{
  // constructors used in the same order as described above:

  std::forward_list<int> first;                      // default: empty
  std::forward_list<int> second (3,77);              // fill: 3 seventy-sevens
  std::forward_list<int> third (second.begin(), second.end()); // range initialization
  std::forward_list<int> fourth (third);            // copy constructor
  std::forward_list<int> fifth (std::move(fourth));  // move ctor. (fourth wasted)
  std::forward_list<int> sixth = {3, 52, 25, 90};    // initializer_list constructor

  std::cout << "first:" ; for (int& x: first)  std::cout << " " << x; std::cout << '\n';
  std::cout << "second:"; for (int& x: second) std::cout << " " << x; std::cout << '\n';
  std::cout << "third:";  for (int& x: third)  std::cout << " " << x; std::cout << '\n';
  std::cout << "fourth:"; for (int& x: fourth) std::cout << " " << x; std::cout << '\n';
  std::cout << "fifth:";  for (int& x: fifth)  std::cout << " " << x; std::cout << '\n';
  std::cout << "sixth:";  for (int& x: sixth)  std::cout << " " << x; std::cout << '\n';

  return 0;
}

运行结果:
在这里插入图片描述


(2) 迭代器
在这里插入图片描述
可以看到没有 那种 cbegin(),cend() 之类的迭代器,因为单向链表嘛,所以不支持反向迭代器。

(3) 容量
在这里插入图片描述
(4) 访问
在这里插入图片描述
每次只能访问头节点,然后 通过头节点,一个一个往后找。

(5) 操作
在这里插入图片描述

  • assign,用新元素替换容器中原有内容。
  • emplace_front ,在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。
  • push_front ,pop_front 是头插,头删
  • emplace_after,在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。和 insert_after() 的功能相同,但效率更高
  • insert_after() ,注意这个是 在指定位置 之后 插入 元素。
  • erase_after(),删除容器中某个指定位置或区域内的所有元素。

6.3 unordered系列

大家可以参考我这篇博客unordered系列


7. 默认成员函数控制

默认的成员函数,大家应该知道,我们定义一个类,类中会生成默认的成员函数。

C++98 是有六个默认的成员函数:

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

c++11 多加了俩个:

  1. 移动拷贝构造函数
  2. 移动赋值运算符重载

C++98的六个默认成员函数生成的原则是:只要我们不显示的定义成员函数,那么 就会 生成类内 默认的成员函数。

C++11的移动拷贝构造函数默认生成的条件很复杂:

  • 如果没有实现移动构造函数,且没有实现析构函数,拷贝构造,拷贝重载中的任意一个,那么编译器会默认生成一个移动构造。
  • 如果没有实现移动赋值重载函数,且没有实现析构函数,拷贝构造,拷贝重载中的任意一个,那么编译器会默认生成一个移动赋值重载。
  • 如果实现移动构造或是移动拷贝重载的任意一个,那么编译器不会自动提供拷贝构造和拷贝赋值。

听上去就感觉很复杂,所以 C++11 允许程序去 控制 是否 生成默认的成员函数,而不是只依据以上的规则,可以人为控制。

7.1 显示缺省函数

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

比如:

class A
{
private:
	int _a;
	int _b;
public:
	A(int a,int b)
		:_a(a),
		_b(b)
	{}
};
int main()
{
	A();
	return 0;
}

这种情况下,因为我们显示的实现了 构造函数,所以默认的构造函数就不生成了。

现在运行就会报错:
在这里插入图片描述

我们可以怎么解决以上问题呢?

(1) 可以重载一个无参数的构造函数

A()
{
 _a =0;
 _b =0;
}

这种方式在C++98中常见,但是不够安全。

(2) 使用关键字default

A() = default;

这就默认生成了构造函数。


7.2 删除默认成员函数

上面是指定生成默认成员函数,这个就是要 删除默认成员函数,也可以说是 禁止生成默认的成员函数。

  • 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。这样确实是没生成默认的构造函数,但是有些复杂。

  • 在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

比如:

这是一个类A,它的拷贝构造,赋值重载都用默认生成的。

class A
{
private:
	int _a;
	int _b;
public:
	A(int a,int b)
		:_a(a),
		_b(b)
	{}
	A() = default;
};

int main()
{
	A a = {1,2};
	A b(a);
	A c = a;
	return 0;
}

先试试 C++98 时的做法:

class A
{
private:
	int _a;
	int _b;
public:
	A(int a,int b)
		:_a(a),
		_b(b)
	{}

	A() = default;
private:
	A(const A& tem);
	A& operator = (const A tem);

};

int main()
{
	A a = {1,2};
	A b(a);
	A c = a;
	return 0;
}

很明显会报错:

在这里插入图片描述

再试试c++11的做法:

A(const A& tem) = delete;
A& operator = (const A tem)= delete;

报错信息:
在这里插入图片描述


8. 右值引用

可以说 C++11 中 右值引用的实现,是很成功的,它提高了 c++的效率,哎呀,这说的有点笼统,但想表达就是 c++11的右值引用 很重要。

8.1 区分左值和右值

  • 左值可以出现在 符号的左边,也可以出现在符号的右边 并且可以取地址
  • 右值可以出现在 符号的右边,不能出现再符号的左边 并且不可以取地址

比如:

    int a;
	const int b = 10;
	int* p = &a;
	int* p1 = new int(2);

以上都是 左值,最直接的就是 可以对它们取地址。


    20;
	a + b;
	add(a + b);

以上都是 右值,不能对它们取地址。


8.2 左值引用和右值引用

C++ 98提出引用,只能对左值引用,就相当于对 左值起别名,它的底层实现是指针。
C++ 11 支持的右值引用。

比如:

    int a;
	const int b = 10;
	int* p = &a;
	int* p1 = new int(2);

	int& s = a;
    const int& s1 = b;
	int*& m = p;
	int*& m1 = p1;

以上都是左值引用,就是对左值起别名。


比如:

    20;
	a + b;

	int&& n = 20;
	int&& n1 = a + b;

这就是 右值引用,用的是&&这个符号,上面 & 是左值引用用的符号。


8.3 交叉引用

就有个问题,左值引用可以引用 右值吗?还有就是 右值引用可以引用 左值吗?

其实 我们在 C++98中,就用过 左值引用 来引用 右值,但不是直接引用:

比如 我们使用的容器string ,我们是不是也用过这样的方式去构造string ,string("hhhhhh")

这样的方式,传参 传的就是 一个右值,但是我们用的是左值引用来接收的:
在这里插入图片描述


所以得出第一个答案:

左值引用可以引用 右值,但是需要是 const 左值引用来接收 右值、

比如:

    const int& n3 = a + b;
	const char& n4 = 'w';

右值引用可以引用 左值吗?说实话,理论上不可以,但是 有一个骚操作可以帮助我们把左值变成右值。怎么说呢?其实还是 右值引用 去引用右值,但是这个右值 是左值变的。那么右值到底可不可以 引用左值?这个答案,我想说 :能,但不完全能。

这个将左值变为右值的函数就是 move()

int&& n5 = move(a);

8.4 右值引用的应用

上面的右值引用的到底有什么作用?直观来说 ,可以支持 给右值 取别名。

  1. 实现移动构造,移动赋值
  2. 给中间临时变量起别名
  3. 实现完美转发
8.4.1 实现移动构造,移动赋值

什么是移动构造,移动赋值?为什么要移动构造,移动赋值?怎么使用移动构造,移动赋值?

移动构造和移动赋值:是c++11 中新增的俩个默认成员函数。

它们的出现,减少了类中的深拷贝,提高了效率。

比如 类的临时变量返回值问题,注意不是引用返回,会用到 移动构造,移动赋值、

先给出一个简易的string类,来帮助我们学习这块知识:

namespace ly
{
	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)
			, _size(0)
			, _capacity(0)
		{
			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()
		{
			//cout << "~string()" << endl;

			delete[] _str;
			_str = nullptr;
		}

		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;
		}

		string operator+(char ch)
		{
			string tmp(*this);
			push_back(ch);

			return tmp;
		}

		
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

很明显,上面并没有 实现 移动构造和移动赋值,也没有默认生成的。

我们来看一下:如果是简单的函数 返回 一个string临时对象,会发生 什么?

ly::string func3()
{
	ly::string str("hello world");
	//cin >> str;

	return str;
}

int main()
{
  ly::string ret = func3();
  return 0;
}

在这里插入图片描述
发生了一次深拷贝,其实是发生两次深拷贝,编译器优化后是一次深拷贝。

图解:
在这里插入图片描述

但是 编译器做了优化,变成了一次深拷贝:

在这里插入图片描述
但是一次深拷贝的代价,也不小。

移动拷贝构造登场:

首先,fun3()函数的返回值,是一个右值。右值 分为纯右值,将亡值。一些表达式 一般都是纯右值,但是对于函数来讲,出来函数作用域的临时变量就会被销魂,如果函数的返回值是函数域内的临时变量,那么 这个临时变量就是一个将亡值,也是一个右值。

返回这个右值 ,需要在内存空间开辟一个空间,拷贝它的值,这是一次深拷贝;然后返回给接收方,又是一次深拷贝。这讲的是没优化的哈。

有没有一种可能?我不做深拷贝,这个将亡值在 出作用域的时候,把值给交换走,只是简单的交换,不做深拷贝?

是可以实现的,那就需要移动拷贝构造,我的参数 需要是一个右值引用来识别右值,简单得来说就是 实现一个新的拷贝构造版本,这个拷贝构造完成的是 值交换,它适用于右值的拷贝构造。

        string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 资源转移" << endl;
			// 仅仅是交换,比深拷贝好多了
			this->swap(s);
		}

再来运行一下程序:

在这里插入图片描述
但是还有一个问题:

如果main函数中这样写,没有编译器的优化:

    ly::string ret;
	ret = func3();

运行结果:

在这里插入图片描述
怎么回事?又出现了深拷贝。

  • 因为有 赋值拷贝,这也是深拷贝。

  • func3() 返回的是一个右值,通过移动构造使得返回时不需要做深拷贝,而是资源转移,但是这次没有编译器的优化,它的资源是转移到临时空间,然后 再从临时空间 通过深拷贝 赋值给 ret。

怎么解决这个问题呢?

移动赋值登场:

        string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 转移资源" << endl;
			this->swap(s);

			return *this;
		}

先看运行结果:

在这里插入图片描述

图解: 来图解一下,以上全部过程

在这里插入图片描述

学到这里,可能有人还是不太懂右值拷贝,右值赋值的意义,我问个问题:如果是一个左值,咱们去拷贝,赋值,敢不敢直接 就是 交换一下值?

肯定是不敢的,因为左值,人家只是给你拷贝一下,赋值一下,人家还存在呢,你直接把人家的值也给交换了,肯定不行,况且 左值的拷贝,赋值,压根就不能改变左值的值,因为人家传参带着 const 。

就是因为,是右值,将亡值,它马上就不存在了,所以交换一下值,没什么毛病,而且不用深拷贝了,很香。


8.4.2 给中间临时变量起别名

其实这个咱们在上面已经用过了,就是给右值起别名。

    string s1;
    string s = s1 + 'w';
	string&& ss = s1 + 's';

string s 是 用 s1 + ‘w’ 构造的新对象,string &ss 是 s1+‘s’ 的别名。

那有个问题:ss是右值的别名,那么 ss的属性是右值还是左值? 验证这个问题,我们可以取一下ss的地址,看看 可不可以取到地址,如果能取到地址,说明 ss是左值,反之为右值。

在这里插入图片描述
答案是可以取到地址,说明 右值引用后,退化为 一个左值。

那么从这里我们也可以看出右值引用的本质,原本右值是不可以取地址的,右值引用其实就是将右值存到一个新建的同类型变量中,变为一个左值。我看书时,有的将 右值引用,使得右值的生命周期变长了,可以这么理解,但是 不过就是把它的值 报存到一个左值变量中罢了。

8.4.3 实现完美转发

有了上面的认识,我们来看看 什么叫做完美转发?听上去还蛮高大上的。

再讲完美转发前,我们先认识一个概念:万能模板

template<typename T>
void PerfectForward(T&& t)
{
		Fun(t);
}

这是函数模板,它的模板参数是T&& t。不要认为 在模板中 这代表 只能匹配右值。这是万能模板,它既可以匹配左值,也可以匹配右值。

那么我们来验证一下,万能模板:

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)
{
		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;
}

运行结果:

在这里插入图片描述
结果有些出乎意料,全都匹配到左值上去了。

提问: 万能模板失效了?传参是右值,为什么会匹配到左值上?

  • 因为,上面也说过,右值引用的本质,是把右值的值保存到一个左值中,再往下传参,传的就不是右值了,而是一个左值。
    在这里插入图片描述

怎么解决这个问题?那就是用完美转发

所谓完美转发,就是 为了保存右值的属性。用的函数是forward()。有人可能会想:既然它退化为一个左值,那么我用move() 也可以将这个左值再转换为右值。但是其实 很欠缺考虑,因为人家 万能模板 ,你传右值是右值引用,你传左值是左值引用。如果人家 传来就是左值,一个左值引用,你再给人家转为右值。是不是就不符合我们预期了。

所以 要用 forward()。这个函数不会影响左值引用的属性,只是将 右值 的属性 保持下去。

那么我们来改一下代码:

template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}

来看运行结果:

在这里插入图片描述
对吧,都匹配正确了。

完美转发的应用,还比较广泛,比如 容器插入 右值,我们用右值引用接收,那么需要保证右值的属性就需要 用forward() 把右值 属性保持下去。

9. lambda表达式

为什么要有lambda表达式?必然是了为了更加便捷的写代码。

仿函数大家应该都知道,它是一个提供了operator () 重载的类,比如 std::sort()要自定义比较就会用到仿函数,还有 优先级队列 std::priority_queue 等等,要自己控制的时候比较的时候都需要用到仿函数。

但是 会不会有点繁琐?假如 排序,我要根据多个方面排序,那我就得实现多个仿函数去实现。

举个例子:

struct product
{
	int _price;
	int _size;
	string _name;
};

struct compre_price
{
	bool operator()(const product& s1,const product& s2)
	{
		return s1._price > s2._price;
	}
};

int main()
{
	product s[] = { {10,20,"手套"},{230,43,"鞋子"},{3,12,"笔"} };
	sort(s,s+sizeof(s)/sizeof(s[0]),compre_price());
	return 0;
}

上面是根据价格去排序,我们看看效果怎么样:

排序前:
在这里插入图片描述
排序后:
在这里插入图片描述
很明显根据价格,排成了降序。

那么我现在要求根据 名字 来排序,好嘛,还得实现一个仿函数:

struct compre_name
{
	bool operator()(const product& s1, const product& s2)
	{
		return s1._name > s2._name;
	}
};

然后再传参给 sort() 进行排序,这显然是 繁琐的,有没有办法 不去实现仿函数 ,就能完成上述功能呢?

lambda表达式登场:

我们先来写代码,后面 会讲其使用规则已经底层原理。

int main()
{
	product s[] = { {10,20,"手套"},{230,43,"鞋子"},{3,12,"笔"} };

	sort(s, s + sizeof(s) / sizeof(s[0]), [](const product& s1, const product& s2)
		                                   ->bool
		                                   {
			                                 return s1._price > s2._price;
		                                   });
	return 0;
}

就是这样的,这是以价格做比较完成的,现在我们实现以名字为比较的版本:

int main()
{
	product s[] = { {10,20,"手套"},{230,43,"鞋子"},{3,12,"笔"} };

	sort(s, s + sizeof(s) / sizeof(s[0]), [](const product& s1, const product& s2)
		                                   ->bool
		                                   {
			                                 return s1._name > s2._name;
		                                   });
	return 0;
}

对吧,只是对代码稍作改动就可以了。

9.1 lambda表达式的格式

[capture-list] (parameters) mutable -> return-type { statement }

  1. [capture-list] 是捕捉列表,它用于捕捉上下文变量,供lambda表达式使用。
  • [] ,空,表示不进行变量捕捉,但是不可以省略。

  • [val] ,表示 以值传递的方式,捕捉某个具体的变量

  • [=] ,表示值传递方式捕获所有父作用域中的变量(包括this)

  • [&val],表示引用传递的方式捕获某个变量

  • [&],表示引用传递的方式捕获所有变量

  • 以上可以组合使用,但是不允许重复使用。
    比如:[a,&b] 意思是 值传递捕获 a,引用捕获 b;[=,&a] 意思是值传递捕获其他变量,引用捕获a;但是 [=,a] 或是 [&,&a] 都是不可以的,因为这是重复值捕获,或是 重复引用捕获。

  1. (parameters) 是参数列表 ,可以理解成普通的函数参数,如果需要传参 那么就需要声明;如果 不需要传参,那么就可以给空,或者直接连() 都省略掉。
  2. mutable ,它是一个修饰;默认情况下 lambda函数是一个const属性的函数,如果加上mutable可以改变其 常量性。如果用 mutable修饰,那么参数列表必须存在。
    int m = 0;
	int n = 0;
	[&, n](int a) {m = ++n + a; } (4);

	cout << m << endl << n << endl;

比如上述代码,n是值传递的,默认是const类型,那么{}中 ++n ,是不可以的。

在这里插入图片描述
但是加上 mutable 后,就可以了:

	[&, n](int a)mutable{m = ++n + a; } (4);
  1. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。注意是返回值类型,返回值类型确定,这种情况下,也可以省略。
  2. { statement }: 这是lambda表达式中的函数体,函数体中可以使用函数参数,也可以使用捕捉的变量。注意函数体为空可以,但是{} 不可以省略。

综上给出lambda表达式 的几种省略形式:

[]{} // 最简单的lambda表达式,但没意义哈
[=]{cout<<a+b<<endl;} // 省略参数列表,和返回类型 
[=](int a) {cout<<a<<endl;} // 省略返回类型,因为没有返回值嘛

/// 以上都默认省略 mutable 

注意: lambda表达式 不可以相互赋值,即便类型相同,但是可以赋值给,类型相同的指针

9.2 lambda表达式的底层原理

其实底层原理,我们来想一想:std::sort()的,第三个参数 是传仿函数,为什么传lambda表达式也可以完成传参?有没有可能 lambda表达式 的底层 是仿函数。

我们通过 反汇编调试 来看一下:

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);
}

这是函数对象的反汇编:

在这里插入图片描述

它是构造了一个函数对象,然后调用函数对象的 operator() 重载。


这是lambda表达式的反汇编:

在这里插入图片描述

它是构造了一个lambda表达式对象,它是一个仿函数类,然后调用lambda表达式中的 operator()重载。

嗯,这就是lambda表达式的底层原理,它其实也是仿函数,只不过是封装到了lambda表达式类中。


10. 线程库

一个编程语言,它的标准库中的函数,可以说是它的宝贵资源之一。C++11 封装了线程库,也就是说 线程也可以面向对象操作了。这是方便程序员操作的,封装成一个类,是比我们自己去调用函数舒服的。我之前一直在Linux环境下 ,进行线程,多线程的学习。windows的线程实现和Linux还不一样,它有自己的线程库函数。通过这一小章,我们来在windows下,进行线程操作。

10.1 线程库的认识

在这里插入图片描述

10.1.1 < atomic > 原子性操作。

构造一个原子性的数据,这个数据可以很多类型,但是不能是浮点数类型。

想维持数据的原子性,一般需要 加锁,但是 如果只是一个数据要保持原子性,那么就需要用到 < atomic >。

    atomic<int> b(0);
	b++;

	int a = 0;
	a++;

比如 多线程对 b和a 进行++,操作,那么b肯定是保持原子性的,a的原子无法保证。

可以看反汇编:

在这里插入图片描述
b的话是去调用 atomic 类中的 operator++。a是三句汇编进行++操作。

如果原子性不懂的话,这里就理解一下吧。因为a++的汇编要执行三步,那么多线程执行时,就有可能被打断 ,从而导致 ++ 进行到一半,被别的线程去执行,等到这个线程再开始执行时发现数据已经变了。这就是 原子性没有保持。

为什么 atomic类中的 operator++是 原子性的呢?这个我没查阅,毕竟人家这个类 就是为了保持原子性的操作的,所以大家只要知道,用atomic构造出的对象,它的操作是原子性的就行了。至于应用后面,会用到的。

10.1.2 < condition_variable> 条件变量

学过多线程的老铁,对这个肯定不陌生。条件变量配合着 互斥锁,就能完成多线程的同步和互斥。

我们来看看 这些接口:

在这里插入图片描述

它的构造函数:

在这里插入图片描述
所以说无参构造就可以了。


它的wait(),也就是在某个条件下开始等待:

在这里插入图片描述
wait()重载了两个 版本,但是都得传参进一个锁,这是为什么呢?

  • 我们来看一下官方文档:
    在这里插入图片描述
    条件变量一般是 在锁得保护下,条件变量等待时,还需要占用锁资源嘛?答案是不需要占用。所以 条件变量下 进行等待时,需要把锁资源释放掉,看上面也写着。

它的唤醒函数:

在这里插入图片描述
在这里插入图片描述


10.1.3 < mutex > 锁

在这里插入图片描述
这些 是 锁 ,自旋锁,等……


10.1.4 < thread > 线程

在这里插入图片描述
线程创建,线程等待……,这些接口 会用就OK了。


10.2 线程的创建和使用

线程创建允许构建无参的,也可以传右值进行构造。

比如:

thread t1;
thread t2("可调用对象","参数");

可调用对象包括:函数指针(函数名),函数对象(仿函数),匿名函数( lambda表达式)。

线程的创建后,需要主线程去等待,等待的方式有两种:

  • join(),线程等待,主线程回收线程的退出信息
  • detach(),线程分离,主线程不需要回收其退出信息,线程运行结束后,直接溜就行

举个例子吧:

void ThreadFunc(int a)
{
	cout << "Thread1" << a << endl;
}
class TF
{
public:
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};

int main()
{
	// 线程函数为函数指针
	thread t1(ThreadFunc, 10);

	// 线程函数为lambda表达式
	thread t2([] {cout << "Thread2" << endl; });

	// 线程函数为函数对象
	TF tf;
	thread t3(tf);

	t1.join();
	t2.join();
	t3.join();

	cout << "Main thread!" << endl;
	return 0;
}

运行结果:

在这里插入图片描述

10.2.1 创建一个线程对一个数进行 ++ 操作
10.2.1.1 简单实现
int number = 0;

void run()
{
  number++;	
}

int main()
{
	thread t1(run);
	t1.join();
	cout << number << endl;
}

我们来看看结果:

在这里插入图片描述
确实是完成了 ++ 操作。

10.2.1.2 函数传参的一些细节(局部变量)

但是这里有个问题就是,用到了全局变量,全局变量是不希望用到工程中的。所以改成对变量++操作,看看效果如何:

void run(int number)
{
	number++;
}

int main()
{
 int x = 0;
 thread t(run, x);
 t.join();
 cout << x << endl;
}

看看结果:

在这里插入图片描述
发现并没有完成++操作,这是什么原因?因为是传值调用,所以不会对局部变量产生影响,C语言基础好些,应该能反应出来,说:应该传址调用,也就是传指针。但是都到C++了,咱们多给几种方案:

(1) 传地址

void run1(int* number)
{
	(*number)++;
}
int main()
{
    int x = 0;
	thread t1(run1,&x);
	t1.join();
}

(2) 传引用

void run2(int& number)
{
	number++;
}
int main()
{
    int x;
    thread t2(run2, std::ref(x));
	t2.join();

}

注意 : 传引用这里用到了一个 函数ref(),它就是传x的引用;这里不能直接传x的引用,只能通过这个函数进行转换。

(3) 利用lambda表达式进行捕捉

int main()
{   
    int x =0;
    thread t3([&x]() {x++; });
	t3.join();
}

对吧,这里一个引用捕捉就完成任务了。

10.2.2 多线程对一个数进行 累加的操作
10.2.2.1 简单实现

上面是一个线程对一个数进行 ++ 操作,现在创建 五个线程对一个数 进行 累加,这样是不是存在线程安全问题呀,用什么解决?利用锁,来维护。

我们先来演示不加锁的情况:

void run(int &x,int n)
{
	int i = 0;
	while (i < n)
	{
		x++;
		i++;
	}
}

int main()
{
	int x = 0;
	vector<thread> vt;
	vt.resize(5);

	
	for (int i = 0; i < 5; i++)
	{
		vt[i] = thread(run,ref(x),1);
	}

	for (int i = 0; i < 5; i++)
	{
		vt[i].join();
	}

	cout << x << endl;
	return 0;
}

运行结果:
在这里插入图片描述
因为是累加到1,然后总共五个线程所以累加最终结果是 5;现在我让每个线程累加这个数字多些,让它出现 问题:

就改一行代码vt[i] = thread(run,ref(x),100000);
看结果:
在这里插入图片描述

10.2.2.2 锁的引入

很奇怪吧,按理说应该是 500000,结果是这样的。非常不人性,那么解决方案是上锁。

mutex mx;

void run(int &x,int n)
{
	int i = 0;
	while (i < n)
	{
		mx.lock();
		x++;
		i++;
		mx.unlock();
	}
}

把锁上在循环里面,或者上到循环外面都可以,但是效率有差别,这个一会分析,我们先来看看 是否解决了上面问题:

在这里插入图片描述
可以,上锁就可以解决这块的问题。

其实把锁上到外面或者是里面,对于这个程序来说本质上就是串行和并行的区别:

在这里插入图片描述


10.2.2.3 原子性操作库 < atomic >的引入

但是可不可以不用锁来管这件事,毕竟我只是对 一个数据 进行 累加操作,昂,可以,那就是用原子性操作库 < atomic > :

void run(atomic<int>& x,int n)
{
	int i = 0;
	while (i < n)
	{
		x++;
		i++;
	}
}

int main()
{
	atomic<int>x = 0;
	vector<thread> vt;
	vt.resize(5);
	for (int i = 0; i < 5; i++)
	{
		vt[i] = thread(run,ref(x),100000);
	}
	for (int i = 0; i < 5; i++)
	{
		vt[i].join();
	}

	cout << x << endl;

	return 0;
}

对吧,就是这样,对一个数据进行原子保护,我建议用< atomic >。


10.2.2.4 lambda表达式进行捕捉

上面对锁的使用,依旧是用到全局变量,不太好对吧,所以改成局部变量的,这就需要用到
lambda表达式了:

int main()
{
    int x = 0;
	int n = 100000;
	int j = 0;
	vector<thread> vt;
	vt.resize(5);
	mutex mx;

	for (int i = 0; i < 5; i++)
	{
		vt[i] = thread([&mx, &x,n,j]()mutable {
		mx.lock();
		while (j < n)
		{
			x++;
			j++;
		}
		mx.unlock(); });
	}
}

10.2.3 锁的考验
10.2.3.1 锁的使用常见问题

其实对锁的使用,挺考验人的,你得考虑死锁的问题,或者你得记得释放锁。尤其是这个释放锁,很可能就没释放掉,为啥没释放掉锁,可能还很纳闷,毕竟我已经写了unlock()了。

我列举俩种可能释放锁失败的例子:

  1. 代码在释放锁前返回:
    在这里插入图片描述

  2. 抛异常,导致锁未释放:

void func(vector<int>& v, int n, int base, mutex& mtx)
{
	try
	{
		// 死锁
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();
			cout << this_thread::get_id() << ":" << base + i << endl;

			// 失败了 抛异常 -- 异常安全的问题
			v.push_back(base+i);
			// 模拟push_back失败抛异常
			if (base == 1000 && i == 888)
				throw bad_alloc();

			mtx.unlock();
		}
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

int main()
{
	thread t1, t2;
	vector<int> vec;
	mutex mtx;

	try
	{	
		t1 = thread(func, std::ref(vec), 1000, 1000, std::ref(mtx));
		t2 = thread(func, std::ref(vec), 1000, 2000, std::ref(mtx));
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	
	t1.join();
	t2.join();
	return 0;
}

比如以上代码,就是 一个线程,出现异常,但是没有释放锁,导致死锁问题,为了模拟这个问题,代码里面主动让它抛了一个异常:

报错了:
在这里插入图片描述

在这里插入图片描述

怎么解决,很简单,在捕获到异常后,释放掉锁:

    catch (const exception& e)
	{
		cout << e.what() << endl;
		mtx.unlock();
	}

10.2.3.2 lock_guard与unique_lock

通过上面的了解,发现了,锁比较难控制,有么有办法,让锁这东西自动去释放呢?就像类一样,不需要的时候,它会去调用它的析构函数。

其实是有解决方法的,那就是:lock_guard与unique_lock。

它俩是C++11采用RAII的方式对锁进行了的封装,也就是 锁的释放 靠它们自己决定,不需要我们手动的去释放。

比如上面那个抛异常的代码我们可以这样写:

void func(vector<int>& v, int n, int base, mutex& mtx)
{
	try
	{
		// 死锁
		for (int i = 0; i < n; ++i)
		{
			lock_guard<mutex>tx(mtx);
			// unique_lock<mutex>tx(mtx);

			cout << this_thread::get_id() << ":" << base + i << endl;

			// 失败了 抛异常 -- 异常安全的问题
			v.push_back(base+i);
			// 模拟push_back失败抛异常
			if (base == 1000 && i == 888)
				throw bad_alloc();
		}
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

我们模拟实现一下lock_guard,它其实就是利用类对象,在释放资源时会自动调用析构函数这一个特性:

template<class T>
class lockguard
{
public:
	lockguard(T& mtx)
		:_mtx(mtx)
	{
		_mtx.lock();
	}

	~lockguard()
	{
		_mtx.unlock();
	}
private:
	T& _mtx;
};

注意这里模拟实现的细节还挺多:

  • 私有成员是 一个 引用,它是为了到时候可以析构传来的锁,所以需要是引用
  • 构造函数的参数我们一般都是 const T& ,但是这里需要是 T& ,不加const因为我们要释放锁,不能设置为const属性。
  • 析构函数,将锁释放掉。

图解:

在这里插入图片描述


10.2.4 两个线程交替打印,一个打印奇数 一个打印偶数(100以内)

讲这个主要是想 带大家认识 条件变量,一起加油!!!

10.2.4.1 简易实现(失败版本)
int main()
{
	int n = 100;
	int i = 0;
	mutex mtx;

	// 偶数-先打印
	thread t1([n, &i, &mtx]{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cout <<this_thread::get_id()<<":"<<i << endl;
			++i;
		}
	});

	// 奇数-后打印
	thread t2([n, &i, &mtx]{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cout << this_thread::get_id() << ":" << i << endl;
			++i;
		}
	});
	// 交替走
	t1.join();
	t2.join();
	return 0;
}

看看结果:

在这里插入图片描述
很明显不是交替打印,所以需要使用条件变量,来控制这块。


10.2.4.2 条件变量

我们先来学习下,它的接口:

在这里插入图片描述
wait():
第一个参数 是 一个unique_lock< mutex >&lck 锁,对吧,这好理解,必须得传入锁,当进程进入wait()状态,它会把锁资源释放掉,等它被唤醒,又会立马获得锁。

第二个参数 是 一个可调用对象,它得返回一个bool值,这个bool值就是我们用来判断是否要被唤醒的条件,而且 wait()底层中,对这个可调用对象是一个while循环判断,防止被伪唤醒。while (!pred()) wait(lck);

在这里插入图片描述
这俩个唤醒函数,一个是唤醒在此条件变量下等待的一个线程,另一个是唤醒在此条件变量下等待的所有线程,使用起来比较简单。


好,有了以上基础,我们就来模拟实现:

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable mtc;
	bool flage = false;

	thread t1([n,&i,&mtx,&mtc,&flage]() 
		                      {
			while (i < n)
			{
				unique_lock<mutex> tx(mtx);
				mtc.wait(tx, [&flage]{return flage;});
				cout << this_thread::get_id() << ":" << i << endl;
				i++;
				flage = false;
				mtc.notify_one();
			}});

	thread t2([n, &i, &mtx, &mtc, &flage]()
		{
			while (i < n)
			{
				unique_lock<mutex> tx(mtx);
				mtc.wait(tx, [&flage] {return !flage; });
				cout << this_thread::get_id() << ":" << i << endl;
				i++;
				flage = true;

				mtc.notify_one();
			}});

	t1.join();
	t2.join();

	return 0;
}

这对lambda表达式的应用需要懂哈,不熟悉的话,会写的很难受。

然后难点就是 wait()中 第二个参数的编写了,也是lambda表达式哈,条件变量先设置为 false:

(1) 线程t1 wait()返回判断为false,然后线程t1 就会陷入等待状态,被阻塞
mtc.wait(tx, [&flage]{return flage;});注意是flage
(2)线程2 wait()返回判断为true,然后线程t2不被阻塞
mtc.wait(tx, [&flage] {return !flage; }); 注意是 !flage
(3)线程2 执行一次后,将flage 设为true,并唤醒线程1,因为flage为true,所以线程1不被阻塞,线程2被阻塞。
(4) 线程1 执行一次后,将flage设为false,并唤醒线程2,因为flage为flase,所以线程2不被阻塞,线程1被阻塞。
(5) 就是这样完成的交替打印。


11. 可变参数列表

可变参数列表是如何实现的呢?其实是通过模板来实现的。

我们最早接触的可变参数列表无非就是 printf(),

我们通过代码来学习去块内容:

template <class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(Args) << endl;
	cout << sizeof...(args) << endl << endl;
	for (size_t i = 0; i < sizeof...(Args); ++i)
	{
		// 无法编译,编译器无法解析
		cout << args[i] << "-";
	}
	cout << endl;
}

int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

先讲点基础知识:

  • template <class ...Args> 这就是模板参数包。
  • void ShowList(Args... args) 这就是函数形参的参数包
  • sizeof...(args) 这是求参数的个数

我现在的要求就是 我传过去参数,要求 函数可以把它们打印出来。很简单的要求哈。但是其实涉及 如何解参数包这一任务,本文给出几种 解包的方式。

先看一下:上面的代码可以完成任务嘛?

在这里插入图片描述
结果是不能,说明不能够 通过下标这种方式来解包。


  1. 通过写递归函数来解包
 //递归终止函数
template <class T>
void ShowList(const T& t)
{
	cout << t << endl << endl;
}

 解析并打印参数包中每个参数的类型及值
template <class T, class ...Args>
void ShowList(T val, Args... args)
{
	cout << typeid(val).name() << ":" << val << endl;
	ShowList(args...);
}
//
int main()
{
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

它是一步一步的来解包,知道剩下一个参数时,去调用递归结束函数,然后开始返回。

通过调试窗口来看看,递归的过程:

在这里插入图片描述
一直递归到 终止函数,然后开始返回:

在这里插入图片描述
通过画图来理解:

在这里插入图片描述


  1. 利用数组解包
template <class T>
void PrintArg(T val)
{
	cout << typeid(T).name() << ":" << val << endl;
}

//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}

这个数组利用的 逗号表达式,逗号表达式是以最后一个值作为返回值的。

所以(PrintArg(args), 0)的返回值 是 0,那么 (PrintArg(args), 0)... ,它会被展开成 (PrintArg(args1), 0),(PrintArg(args2), 0) ……(PrintArg(argsn), 0)。为什么要这样做呢?其实是因为 c++的数组只能保持一种类型的数据,所以利用逗号表达式,使得数组 既可以执行函数 又能最终以 0 被保存。


  1. 其实还是利用数组解包,但是换个方式
template <class T>
int PrintArg(T val)
{
	T copy(val);
	cout << typeid(T).name() << ":" << val << endl;

	return 0;
}

//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { PrintArg(args)... };
	cout << endl;
}

上面数组是利用的是 逗号表达式 ,目的是让数组中的元素一致,但是利用函数的返回值,也可以作到这一点。上面的逗号表达式,函数返回值,使得数组中的元素都是 int整型,当然这个类型是根据数组定的,我们当然还可以定义成其他类型的数组。

比如:

char arr[] ={(printArg(args),'a')...};

template <class T>
char PrintArg(T val)
{
	T copy(val);
	cout << typeid(T).name() << ":" << val << endl;

	return 'a';
}

char arr1[] = {printArg(args)...};

总结:可变参数列表的实现,难点不在于定义一个多参数模板,而是在于如何拿出参数,也就是 解包。给出的方案总的来说有两个,也就是 递归(注意写终止函数),数组(注意数组的元素类型一致)。


12. 包装器

包装器,它是将可调用对象包装成容器,方便程序员去操作。为什么要封装成容器呢?因为在某些情况下,需要对函数 进行一些特殊的操作,但是 重载函数比较 费劲,比如想要操作函数的参数等。还是得看代码,才能 理解包装器 的妙处。

12.1 可调用对象

先得搞清楚什么是可调用对象:

  1. 函数指针,普通函数
  2. lambda表达式,匿名函数
  3. 仿函数,函数对象

函数指针用起来比较晦涩,难用,所以用的较少。 lambda表达式 用起来挺挺方便。仿函数是多用于模板参数,也很方便。

12.2 function包装器(一般包装)

function包装器,它是常用于将可调用对象,封装成一个容器。它方便在可以 使得 可调用对象变为统一的类型 function< >,还有就是 它还能方便我们去简化代码。

先来讲讲它的用法:

function 它的本质是一个类模板。
原型:

template<class Ret,class... Args>
class function<Ret(Args)>

Ret 是函数返回类型,Args 是函数的参数列表。

我们来看一段代码:

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);
}

这是个函数模板,里面有一个静态变量 count 它可以帮助我们看到,实例化出多少份函数。

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;
	}
};

class A
{
public:
	A() = default;
	static double func(double a)
	{
		return a / 4;
	}
	double func_(double a)
	{
		return a / 5;
	}
};

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;
}

上面的代码,应该会实例化出三份函数,因为传的可调用对象都不一致。我们来看看结果:

在这里插入图片描述
可以看到 count 的地址都不一样,所以明显是 实例化出来三份,但是 我有个问题: 需要实例化出三份嘛?细心点可以发现,函数指针,函数对象,匿名函数 它们三个的 返回值,函数参数列表的类型都是完全一样的。我可以用function 进行包装,使得它们三个类型都是 function类型,从而使得useF() 函数模板,实例出一份函数。

    function<double(double)> f1 = f;
	
	cout << useF(f1, 11.11) << endl;
	function<double(double)> f2 = Functor();
	cout << useF(f2, 11.11) << endl;

	function<double(double)> f3 = [](double d) ->double { return d / 4; };
	cout <<useF(f3,11.11) << endl;

看运行结果:

在这里插入图片描述
很明显是实例化成了一份usef() 函数。

function包装器可以包装函数,当然也可以包装类内的函数,这里有些注意事项:

这是一个类A,它有静态成员函数func() 和 成员函数func_();

class A
{
public:
	A() = default;
	static double func(double a)
	{
		return a / 4;
	}
	double func_(double a)
	{
		return a / 5;
	}
};

使用function 进行包装:

   function<double(double)> f4 = A::func;
	
	cout << f4(11.11) << endl;

	function<double(A,double)> f5 = &A::func_;

	cout << f5(A(), 11.11) << endl;
	

类中静态成员函数的包装,只需要指定类域就完事了;但是成员函数的包装,需要将类名作为函数参数列表的第一个参数,因为 成员函数的参数里有this指针。并且 类域前需要加上符号&。使用时,还得传一个类的匿名对象。

12.3 function包装器(bind包装)

function包装器就是对函数的包装,包装后的对象的功能,用法和以前保持一致,这不是包装后的类型变为了 function<>;但是bind包装, 它对函数进行包装后形成的新对象,可能用法和之前的函数不一样了,对,它可能会对参数做出一些调整,比如 加一个默认参数,改变参数顺序等等。所以 bind包装后,它可以 对原有函数的用法 做出一些调整。

12.3.1 调整参数顺序
int SubFunc(int a, int b)
{
	return a - b;
}

int main()
{
function<int(int, int)> ff1 = bind(SubFunc, placeholders::_1, placeholders::_2);
function<int(int, int)> ff2 = bind(SubFunc, placeholders::_2, placeholders::_1);
	cout << ff1(1, 2) << endl;
	cout << ff2(1, 2) << endl;
}

ff1和ff2都是bind的同一个函数,但是我对参数的顺序做出了调整。

我们来看结果:

在这里插入图片描述

12.3.2 固定默认的参数
int SubFunc(int a, int b)
{
	return a - b;
}

int main()
{
 	function<int(int)> ff3 = bind(SubFunc,placeholders::_1,10);
	cout << ff3(2) << endl;
}

这就相当于每次传进来的数据都 减去 10。

注意事项:

在这里插入图片描述
也就是说,你绑定的参数列表中 只有一个参数,那么后面placeholders也只能操作_1,表示第一位参数。

假如这样搞:

function<int(int)> ff3 = bind(SubFunc,placeholders::_2,10);

毫无疑问会报错:

在这里插入图片描述
out of bounds,也就是超出范围了。

上述我们在稍微操作一下,要求 是 10 - 传参,也就是换一下顺序,那也简单了吧:

	function<int(int)> ff3 = bind(SubFunc,10,placeholders::_1);

所以说,对参数都调整是可以组合使用的。

12.3.3 调整参数个数

其实上面也是调整参数的个数,但下面讲的例子还不太一样,我们这次是要调整类的中函数的参数个数。我们之前用function普通包装,那么还得传参一个默认对象对吧,感觉有点小麻烦。来用bind包装操作一些

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};

int main()
{
 function<int(Sub, int, int)> f4 = &Sub::sub;
	cout << f4(Sub(), 10, 3) << endl;
	
	function<int(int, int)> f5 = bind(&Sub::sub, Sub(), placeholders::_1,    placeholders::_2);
	cout << f5(10, 3) << endl;
}

就是在bind中默认绑定一个类的匿名对象。操作很简单。

但是 我想出点难题,我要求在此基础上,继续调整函数参数:函数的调用 默认是一个参数,要求每次都是参数数据 减去 5。

答案:

function<int(int)> f6 = bind(&Sub::sub, Sub(), placeholders::_1, 5);

对吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

动名词

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值