C++ 11

news2024/10/5 18:30:28

文章目录

    • 1. 列表初始化
      • 1.1 列表初始化的使用格式
        • 1.1.1 内置类型
        • 1.1.2 自定义类型的列表初始化
      • 1.2 列表初始化的本质
    • 2. 变量类型的推导
      • 2.1 auto 关键字
      • 2.2 decltype类型推导
    • 3. 范围for
    • 4. final与override
    • 5. 智能指针
    • 6. 新增容器
      • 6.1 静态数组array
      • 6.2 单向链表 forward_list
      • 6.3 unordered系列
    • 7. 默认成员函数控制
      • 7.1 显示缺省函数
      • 7.2 删除默认成员函数
    • 8. 右值引用
      • 8.1 区分左值和右值
      • 8.2 左值引用和右值引用
      • 8.3 交叉引用
      • 8.4 右值引用的应用
        • 8.4.1 实现移动构造,移动赋值
        • 8.4.2 给中间临时变量起别名
        • 8.4.3 实现完美转发
    • 9. lambda表达式
      • 9.1 lambda表达式的格式
      • 9.2 lambda表达式的底层原理
    • 10. 线程库
      • 10.1 线程库的认识
        • 10.1.1 < atomic > 原子性操作。
        • 10.1.2 < condition_variable> 条件变量
        • 10.1.3 < mutex > 锁
        • 10.1.4 < thread > 线程
      • 10.2 线程的创建和使用
        • 10.2.1 创建一个线程对一个数进行 ++ 操作
          • 10.2.1.1 简单实现
          • 10.2.1.2 函数传参的一些细节(局部变量)
        • 10.2.2 多线程对一个数进行 累加的操作
          • 10.2.2.1 简单实现
          • 10.2.2.2 锁的引入
          • 10.2.2.3 原子性操作库 < atomic >的引入
          • 10.2.2.4 lambda表达式进行捕捉
        • 10.2.3 锁的考验
          • 10.2.3.1 锁的使用常见问题
          • 10.2.3.2 lock_guard与unique_lock
        • 10.2.4 两个线程交替打印,一个打印奇数 一个打印偶数(100以内)
          • 10.2.4.1 简易实现(失败版本)
          • 10.2.4.2 条件变量
    • 11. 可变参数列表
    • 12. 包装器
      • 12.1 可调用对象
      • 12.2 function包装器(一般包装)
      • 12.3 function包装器(bind包装)
        • 12.3.1 调整参数顺序
        • 12.3.2 固定默认的参数
        • 12.3.3 调整参数个数

前言: 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);

对吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/11977.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【问卷调查发布系统的设计与实现】

系列文章目录 在当前社会&#xff0c;随着信息化的高速发展&#xff0c;收集数据的传统方法——问卷调查法也在发生改变。此问卷调查系统&#xff0c;可以帮助用户在短时间内创建收集数据的问卷&#xff0c;目的是突出高效性、绿色性以及便捷性。在设计过程中&#xff0c;分析…

web网页设计期末课程大作业:漫画网站设计——我的英雄(5页)学生个人单页面网页作业 学生网页设计成品 静态HTML网页单页制作

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 ⚽精彩专栏推荐&#x1…

Charles使用教程

目录预备知识1.HTTP调试代理工具原理2.Charles简介实验目的实验环境实验步骤一实验步骤二实验步骤三预备知识 1.HTTP调试代理工具原理 HTTP调试代理工具广泛应用于web程序开发、安全测试、流量分析等工作。HTTP调试代理工具工作于TCP/IP参考模型中的应用层&#xff0c;浏览器…

Docker容器的5个实用案例

Docker 是一个开源平台&#xff0c;可以轻松地为任何应用创建一个轻量级的、 可移植的、自给自足的容器。大多数 Docker 容器的核心是在虚拟化环境中运行的轻量级 Linux 服务器。 Docker Linux 容器有什么实际用例吗&#xff1f;现在让我们一探究竟。 为什么使用 Docker? D…

力扣(LeetCode)792. 匹配子序列的单词数(C++)

二分查找 直观思考&#xff0c;本题可以将 wordswordswords 中每个单词 wordwordword 依次和目标字符串 sss 比较&#xff0c;检查是否为子串。时间复杂度 n∑i0m−1wordsin\times \sum_{i0}^{m-1}words_in∑i0m−1​wordsi​ nnn 是 sss 的长度, mmm 是 wordswordswords 的长…

壁纸号的玩法,拿出来收费未免也太坑人了,所以,直接上教程。

网上关于斗音变现的攻略写得比较少&#xff0c;可以理解为目前仍是风口&#xff0c;都在闷声发大财&#xff0c;虽然我也做知识付费&#xff0c;但是这壁纸号的玩法&#xff0c;拿出来收费未免也太坑人了…… 所以&#xff0c;直接上教程…… 一、准备斗音号 这一块不用多说&…

数据结构之实现队列

文章目录前言1.队列的相关介绍1.队列的定义2.队列的实现方式2.队列具体实现1.队列声明定义2.队列的接口1.初始化接口2.数据的插入和删除3.获取队头元素和队尾元素4.获取队列元素个数和队列判空以及队列3.总结前言 之前谈到了栈的实现&#xff0c;现在来说说另一种数据结构——…

[hadoop全分布部署]虚拟机Hadoop集群配置/etc/hosts、配置无密码登录(SSH)

&#x1f468;‍&#x1f393;&#x1f468;‍&#x1f393;博主&#xff1a;发量不足 个人简介&#xff1a;耐心&#xff0c;自信来源于你强大的思想和知识基础&#xff01;&#xff01; &#x1f4d1;&#x1f4d1;本期更新内容&#xff1a;虚拟机Hadoop集群配置/etc/hosts…

Centos 7下安装php+mysql+nginx+wordpress教程新版

安装zsh+oh-my-zsh 安装zsh的原因是因为不喜欢自带的ssh工具,感觉没有这个好用,我最常用的就是记忆功能,比如输入某个字母,按上下键会自动补全已经使用过的命令,安装也很简单,一条命令搞定,他的扩展也很多,这里只讲最简单的安装,当然也可以不需要安装。 执行yum inst…

Linux基本指令

这一章我们将讲解在Linux系统下&#xff0c;一些基本指令的用法和功能. 后面有一些重要的指令我们将单独讲解. 目录 ls 指令 pwd 指令 cd 指令 touch 指令 mkdir 指令★ rmdir 指令 && rm指令★ man 指令★ cp 指令 ★ mv 指令★ cat && tac指令 e…

nodejs+vue毕业生就业知道信息平台系统

大学毕业生招聘系统分三个身份登录&#xff0c;一个学生&#xff0c;一个管理员&#xff0c;一个是企业用户。学生可以注册登录管理自己的简历,应聘职位,企业用户可以发布招聘&#xff0c;收到应聘信息,查看学生简历,收藏学生简历,而管理员可以修改任何信息。 管理员模块有: 1.…

【8-数据库表结构的创建后台管理系统的搭建】

一.知识回顾 【0.三高商城系统的专题专栏都帮你整理好了&#xff0c;请点击这里&#xff01;】 【1-系统架构演进过程】 【2-微服务系统架构需求】 【3-高性能、高并发、高可用的三高商城系统项目介绍】 【4-Linux云服务器上安装Docker】 【5-Docker安装部署MySQL和Redis服务】…

OPSS-PEG-N3,OPSS-PEG-azide,巯基吡啶-PEG-叠氮化学试剂供应

1、名称 英文&#xff1a;OPSS-PEG-N3&#xff0c;OPSS-PEG-azide 中文&#xff1a;巯基吡啶-聚乙二醇-叠氮 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Azide PEG Orthopyridyl disulfide (OPSS) PEG 4、分子量&#xff1a;可定制&#xff0c;2K 巯基吡啶-PEG-叠…

海量短视频打标问题之多模态机器学习

引言 接着讲&#xff0c;既然我们是给视频打标签&#xff0c;那么肯定就不能只局限于图像上做文章。视频文件包含的信息很多&#xff0c;一个短视频除了有一帧一帧的图像&#xff0c;还有声音信息&#xff0c;甚至还有字幕或者用户打的标签和文字评论之类的这些信息&#xff0…

第2关:ZooKeeper配置

配置项介绍 基础配置 tickTime&#xff1a;Client和Server通信心跳数。 Zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔&#xff0c;也就是每隔tickTime的时间就会发送一个心跳。tickTime以毫秒为单位。 initLimit&#xff1a;LF初始通信时限。 集群中的followe…

muduo库的高性能日志库(二)——LogStream文件

目录概述FixBuffer类&#xff08;模板缓冲区&#xff09;LogStream类LogStream.hLogStream.cc十进制整数转化为字符串地址&#xff08;指针&#xff09;数据转换为16进制字符串浮点类型数据转化为字符串Fmt类C单元测试框架&#xff08;简略&#xff09;什么是单元测试常用测试工…

用了CDN就一定比不用更快吗?

对于开发同学来说&#xff0c;CDN这个词&#xff0c;既熟悉又陌生。 平时搞开发的时候很少需要碰这个&#xff0c;但却总能听到别人提起。 我们都听说过它能加速&#xff0c;也大概知道个原因&#xff0c;但是往深了问。 用了CDN就一定比不用更快吗&#xff1f; 就感觉有些…

C++ Reference: Standard C++ Library reference: Containers: deque: deque: cbegin

C官网参考链接&#xff1a;https://cplusplus.com/reference/deque/deque/cbegin/ 公有成员函数 <deque> std::deque::cbegin const_iterator cbegin() const noexcept;返回指向开始的常量迭代器 返回指向容器第一个元素的const_iterator。 const_iterator是指向const内…

大一新生HTML期末作业,实现登录页面

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

js+贝塞尔曲线+animate动画

文章目录一 介绍二 示例1阶贝塞尔曲线2阶贝塞尔曲线3阶贝塞尔曲线:4/n阶贝塞尔曲线三 封装和使用bezier.jsApp.jsxApp.scss一 介绍 贝塞尔曲线(Bzier curve)&#xff0c;又称贝兹曲线或贝济埃曲线&#xff0c;是应用于二维图形应用程序的数学曲线。 下面是我们最常用到bezier曲…