C++ 11
创始人
2024-01-28 06:24:34
0

文章目录

    • 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++ 98int a[] = { 1,2,3,4,5 };vector aa;for (int i =1 ;i<=5;i++){aa.push_back(i);}

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

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

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

    vector a1 = { 1,2,3,4,5 };list l1 = { 1,2,3,4,5 };map 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 v{ 1,2,3,4,5 };map 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 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 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 m;std::map ::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当 模板

	vectors;

在这里插入图片描述
(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。

   vectorvc{ 1,2,3,4,5,6,7,8,9,10 };vector::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 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 first;                      // default: emptystd::forward_list second (3,77);              // fill: 3 seventy-sevensstd::forward_list third (second.begin(), second.end()); // range initializationstd::forward_list fourth (third);            // copy constructorstd::forward_list fifth (std::move(fourth));  // move ctor. (fourth wasted)std::forward_list sixth = {3, 52, 25, 90};    // initializer_list constructorstd::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
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
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
void PerfectForward(T&& t)
{Fun(std::forward(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<cout<

注意: 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);// lamberauto 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 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 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& x,int n)
{int i = 0;while (i < n){x++;i++;}
}int main()
{atomicx = 0;vector 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 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& 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 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& v, int n, int base, mutex& mtx)
{try{// 死锁for (int i = 0; i < n; ++i){lock_guardtx(mtx);// unique_locktx(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 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 lock(mtx);cout <while (i < n){unique_lock 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 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 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 
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 这就是模板参数包。
  • void ShowList(Args... args) 这就是函数形参的参数包
  • sizeof...(args) 这是求参数的个数

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

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

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


  1. 通过写递归函数来解包
 //递归终止函数
template 
void ShowList(const T& t)
{cout << t << endl << endl;
} 解析并打印参数包中每个参数的类型及值
template 
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 
void PrintArg(T val)
{cout << typeid(T).name() << ":" << val << endl;
}//展开函数
template 
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 
int PrintArg(T val)
{T copy(val);cout << typeid(T).name() << ":" << val << endl;return 0;
}//展开函数
template 
void ShowList(Args... args)
{int arr[] = { PrintArg(args)... };cout << endl;
}

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

比如:

char arr[] ={(printArg(args),'a')...};template 
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 function

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

我们来看一段代码:

template
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}

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

template
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) << endlreturn 0;
}

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

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

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

看运行结果:

在这里插入图片描述
很明显是实例化成了一份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 f4 = A::func;cout << f4(11.11) << endl;function 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 ff1 = bind(SubFunc, placeholders::_1, placeholders::_2);
function 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 ff3 = bind(SubFunc,placeholders::_1,10);cout << ff3(2) << endl;
}

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

注意事项:

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

假如这样搞:

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

毫无疑问会报错:

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

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

	function 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 f4 = &Sub::sub;cout << f4(Sub(), 10, 3) << endl;function f5 = bind(&Sub::sub, Sub(), placeholders::_1,    placeholders::_2);cout << f5(10, 3) << endl;
}

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

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

答案:

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

对吧。

相关内容

热门资讯

老年人运动常见问题,答案在这儿...   如今,越来越多的老年人加入到运动健身的行列。在运动的过程中,老年人常常会遇到一些问题,下面,我们...
免费学前教育迈出关键步伐 惠民...   学前教育关乎千家万户的切身利益。  在今年政府工作报告中,“逐步推行免费学前教育”曾受到广泛关注...
古巴举行“7·26国家起义日”... 古巴“7·26国家起义日”纪念活动26日在古巴中部城市谢戈德阿维拉举行。在回顾古巴从独立到革命胜利历...
央广财评|财政政策更加积极 ...   近日披露的上半年财政收支情况显示,全国一般公共预算支出达到14.13万亿元,同比增长3.4%。其...
新央企中国雅江集团,领导班子亮...   据中国长江三峡集团有限公司官微“三峡小微”7月26日消息,7月19日,中国三峡集团董事长、党组书...
河北阜平遭遇强降雨 致2死2失...   新华社石家庄7月26日电(记者 杜一方、张硕)7月25日至26日,河北省保定市阜平县遭遇强降雨。...
我国将逐步推行免费学前教育 如...   近日召开的国务院常务会议部署逐步推行免费学前教育有关举措。会议指出,逐步推行免费学前教育是涉及千...
国务院食安办部署加强暑期、汛期...   新华社北京7月26日电 记者26日从市场监管总局获悉,国务院食安办近日印发通知,要求各地食安办切...
石榴花开 籽籽同心丨湖北姑娘在...   石榴云/新疆日报讯(记者 甘兴华报道)七月的哈巴河县彩虹布拉克景区,阳光穿透云层温柔地铺展在起伏...
“中国式现代化的万千气象”网络...   7月25日,“中国式现代化的万千气象”网络名人新疆行活动走进尉犁县兴平镇达西村,感受这里繁荣富裕...