【STL算法】for_each源码刨析及函数对象本质刨析

简介: 【STL算法】for_each源码刨析及函数对象本质刨析

前言

本文中将通过STL中的for_each算法为例来深度分析函数对象的本质,以及如何通过分析STL源码来学习STL中的容器、迭代器和算法。


提示:下面将通过编程实战手把手教学如何通过分析源码来学习算法并写出自己的函数对象。

一、搭建一个测试框架

使用STL中的算法要包含头文件<algorithm>,下面搭建一个测试案例,创建一个int型的vector数组,并装入10个随机数,然后自己实现一个遍历容器的函数,对定义好的容器进行遍历打印。

代码如下:

1. #include <iostream>
2. using namespace std;
3. 
4. #include <vector>
5. #include <algorithm>
6. 
7. void print_vector_int(vector<int>& v)
8. {
9.  //for(vector<int>::iterator it = v.begin(); it != v.end(); it++)
10.   //{
11.   //  cout << *it << " ";
12.   //}
13.   for (unsigned int i = 0; i < v.size(); i++)
14.   {
15.     cout << v.at(i) << " ";
16.   }
17.   cout << endl;
18. }
19. 
20. int main()
21. {
22.   vector<int> V;
23.   for (int i = 0; i < 10; i++)
24.   {
25.     V.push_back(rand());
26.   }
27.   print_vector_int(V);
28. system("pause");
29.   return 0;
30. }

二、分析for_each源码

1.分析for_each的函数参数和返回值

一个函数最重要的就是他的参数和返回值,分析函数的参数和返回值是深刻理解这个函数功能的重要前提。我们可以通过VS的转到定义功能来查看for_each函数的原型,代码如下:

1. template <class _InIt, class _Fn>
2.  _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
3.    _Adl_verify_range(_First, _Last); 
4.    auto _UFirst      = _Get_unwrapped(_First); 
5.    const auto _ULast = _Get_unwrapped(_Last); 
6.    for (; _UFirst != _ULast; ++_UFirst) { 
7.      _Func(*_UFirst); 
8.    }
9. 
10.     return _Func; //返回函数对象
11.   }

通过源码可以看出,这个函数有三个参数,第一个第二个参数应该是一个迭代器,第三个参数是一个函数对象(根据template后面的类型说明可以看出)。那么我们应该先定义一个函数对象,然后传入for_each实现功能。

既然要定义函数对象,根据我们前一篇文章的分析,函数对象就是一个重载了函数调用操作符()的类,不了解的请查看

image.png

首先我们应该定义一个类,并在类中实现函数调用操作符()的重载,这就涉及到我们的重载函数的接口应该是什么样子的,包括返回值、参数。根据函数重载的知识我们知道,返回值不是判断函数重载的标准,函数重载是根据函数参数的类型、个数、顺序等来决定的。也就是说函数重载的判断标准只与参数有关,因此,我们在重载函数调用操作符时应该重点关注函数参数。

我们要知道,类中重载的函数调用操作符()是在for_each中使用的,所以,要想知道重载函数的原型,我们应该去for_each源码中去分析,我们看for_each源码的return前一句

_Func(*_UFirst);

for_each函数内部把一个*变量传给了函数对象,由此可见,我们重载的函数调用操作符函数应该只有一个参数。下面在分析这个参数的类型,我们继续看源码

1. auto _UFirst      = _Get_unwrapped(_First); 
2. const auto _ULast = _Get_unwrapped(_Last);

从这里可以看出,_UFirst应该是一个指针,它指向了迭代器_First的位置,那么*_UFirst就是对指针解引用,他应该是容器中的元素(迭代器所指向的元素),也就是说我们需要重载的函数的参数类型是容器的元素的类型。这样我们就确定好了重载函数的参数。

通过for_each源码分析可知,在for_each内部并没有用到_Func(也就是函数对象)的返回值,我们可以把它定义为void类型,这样我们需要重载的函数调用操作符的函数接口就有了。

2.定义函数对象

根据重载函数接口

void operator()(_Type t); //_Type和容器元素类型有关

我们可以定义一个类,并重载括号操作符,实现对容器元素+1的操作,另外在遍历容器的时候记录容器元素个数

1. template<typename _MyType>
2. class MySort
3. {
4. public:
5.  MySort()
6.  {
7.    this->m_count = 0;
8.  }
9. public:
10.   void operator()(_MyType& t) //怎么确定这个函数对象的参数呢?
11.   {
12.     t++;
13.     this->m_count++;
14.   }
15. public:
16.   int get_count()
17.   {
18.     return m_count;
19.   }
20. private:
21.   int m_count; //记录容器中元素个数
22. };

这样,使用for_each的时候,容器中每个变量都会进入这个仿函数,通过私有属性m_count就可以记录总共调用了仿函数多少次,也就是容器中元素的个数。

这里有一点要注意,就是为什么要自定义一个无参构造函数并将私有属性m_count置为0,如果我们不这么做的话,当我们使用类MySort定义对象的时候,编译会报错。

1. MySort m_sort1;
2. 
3. //错误 C4700 使用了未初始化的局部变量“m_sort1”

这样我们便可以在主函数中添加以下代码进行测试

1. for_each(V.begin(), V.end(), MySort<int>()); //使用匿名函数对象
2. print_vector_int(V);

编译运行,可以看到功能已经实现。

3.for_each函数的返回值

在前面定义类的时候我们定义了一个私有属性m_count来记录容器元素个数,那么我们继续添加以下代码进行测试

1. MySort<int> m_sort1; //MySort有私有变量m_count,所以需要提供构造函数初始化m_count
2. for_each(V.begin(), V.end(), m_sort1);
3. cout << "计数:" << m_sort1.get_count() << endl;
4. print_vector_int(V);

并编译运行,查看打印结果

我们看到计数值显示0,这和我们定义的10个元素的容器不符,那么问题出现在哪呢,我们继续看for_each的接口原型

_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func)

问题就出现在这里,函数参数_Func是一个_Fn类型的元素,我们传进的实参m_sort1也是一个元素,但是实参和形参是两个不同的元素,我们把实参m_sort1复制给了形参_Func,在for_each函数内部对形参_Func的属性m_count进行了++运算,但是这和实参m_sort1毫无关系,实参m_sort1的m_count还是0。

注:实参和形参是两个完全不同的变量,在函数调用的时候只是把实参复制给了形参,实参还是实参,形参就是形参,他们两个没有任何关系。要想通过函数参数(形参)改变实参,就要使用引用或者指针(指针做函数参数间接修改实参)。

那么我们怎么使用m_count这个私有属性来打印容器元素个数呢,继续看源码,看for_each函数的返回值

return _Func;

for_each的返回值是一个_Fn 类型的_Func,也是函数的参数_Func。也就是说,形参通过返回值的形式被for_each函数返回了出来,那么我们便可以这么用,继续添加如下代码

1. MySort<int> m_sort2; 
2. m_sort2 = for_each(V.begin(), V.end(), m_sort2);
3. cout << "计数:" << m_sort2.get_count() << endl;
4. print_vector_int(V);

编译运行

我们看到,计数为10,与容器内元素个数相同。

这里应注意,我们用m_sort2来接for_each的返回值时,因为for_each返回的是一个元素,他会把这个元素复制给m_sort2并调用拷贝构造函数,因为我们的类中没有指针,不涉及深拷贝浅拷贝问题,所以不必写深拷贝构造函数,使用默认的浅拷贝构造函数即可。但是,如果类中有指针等涉及深拷贝问题的情况时,一定要手动实现深拷贝构造函数。 函数对象实参传递给for_each的形参时也会调用拷贝构造函数,这里可以参考拷贝构造函数调用时机(实参初始化形参)。

4.for_each源码浅析

最后附上本人对for_each源码的理解,解析在代码注释中。

1. template <class _InIt, class _Fn>
2. _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { //函数对象实参传递给形参调用拷贝构造函数
3. // perform function for each element [_First, _Last)
4.  _Adl_verify_range(_First, _Last); //范围:容器迭代器 begin 到 end
5.  auto _UFirst      = _Get_unwrapped(_First); //指针_UFirst指向迭代器_First的位置
6.  const auto _ULast = _Get_unwrapped(_Last); //指针_ULast迭代器_Last的位置
7.  for (; _UFirst != _ULast; ++_UFirst) { //遍历容器
8.    _Func(*_UFirst); //把容器元素逐个放入函数_Func作为其函数参数
9.  }
10. 
11.   return _Func; //返回函数对象
12. }

三、函数对象的本质

1.函数对象的回调行为

通过上面的讲解,已经实现了自己的函数对象,那么函数对象到底是什么呢?

首先函数对象是一个类对象,它是一个重载了函数调用操作符的类所定义的对象。这个类中必须包含函数调用操作符()的重载,其次它可以有自己的成员属性和成员方法,比如,上面我们定义的类中除了有函数调用操作符的重载,还有私有属性m_count和成员方法get_count()。

在for_each中对函数对象的使用上来看,我们不难发现,函数对象的行为很像一个函数,确切的说很像一个回调函数(C语言中函数指针做函数参数),所以它也叫仿函数。下面我们用一个普通函数来做for_each的参数,测试我们的猜想。

首先,定义一个模板函数

1. template<typename T>
2. void my_add(T& t)
3. {
4.  t++;
5. }

我们把这个函数做参数,并在main函数中继续添加如下测试代码

1. for_each(V.begin(), V.end(), my_add<int>);
2. print_vector_int(V);

编译运行,查看打印结果

可以看到,通过普通的模板函数,我们也实现了函数对象的功能,对容器中每个元素遍历并加1。由此可见,函数对象确实和回调函数有着类似的功能。

C语言回调函数可以参考我的文章

image.png

2.函数对象和回调函数的区别

既然回调函数和函数对象可以实现相同的功能,那么为什么要用函数对象呢?

首先,我们知道,函数对象是一个重载了()操作符的类所定义的对象,既然是类对象,那么他便可以拥有自己的属性和方法。就像前面我们通过类的私有属性m_count来计数容器中的元素个数,并通过成员函数get_count()来返回私有属性m_count。类中可以封装属性和方法,通过类对象做函数参数可以把属性和方法一块传入调用函数中,这是普通函数所不具备的。

四、完整代码

最后附上完整代码

1. #include <iostream>
2. using namespace std;
3. 
4. #include <vector>
5. #include <algorithm>
6. 
7. void print_vector_int(vector<int>& v)
8. {
9.  //for(vector<int>::iterator it = v.begin(); it != v.end(); it++)
10.   //{
11.   //  cout << *it << " ";
12.   //}
13.   for (unsigned int i = 0; i < v.size(); i++)
14.   {
15.     cout << v.at(i) << " ";
16.   }
17.   cout << endl;
18. }
19. 
20. template<typename _MyType>
21. class MySort
22. {
23. public:
24.   MySort()
25.   {
26.     this->m_count = 0;
27.   }
28. public:
29.   void operator()(_MyType& t) //怎么确定这个函数对象的参数呢?
30.   {
31.     t++;
32.     this->m_count++;
33.   }
34. public:
35.   int get_count()
36.   {
37.     return m_count;
38.   }
39. private:
40.   int m_count; //记录容器中元素个数
41. };
42. 
43. template<typename T>
44. void my_add(T& t)
45. {
46.   t++;
47. }
48. 
49. int main()
50. {
51.   vector<int> V;
52.   for (int i = 0; i < 10; i++)
53.   {
54.     V.push_back(rand());
55.   }
56.   print_vector_int(V);
57. 
58.   /*
59.   template <class _InIt, class _Fn>
60.   _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
61.     _Adl_verify_range(_First, _Last); //范围:容器迭代器 begin 到 end
62.     auto _UFirst      = _Get_unwrapped(_First); //指针_UFirst指向迭代器_First的位置
63.     const auto _ULast = _Get_unwrapped(_Last); //指针_ULast迭代器_Last的位置
64.     for (; _UFirst != _ULast; ++_UFirst) { //遍历容器
65.       _Func(*_UFirst); //把容器元素逐个放入函数_Func作为其函数参数
66.     }
67. 
68.     return _Func; //返回函数对象
69.   }
70.   */
71.   for_each(V.begin(), V.end(), MySort<int>()); //匿名对象
72.   print_vector_int(V);
73. 
74.   MySort<int> m_sort1; //MySort有私有变量m_count,所以需要提供构造函数初始化m_count
75.   for_each(V.begin(), V.end(), m_sort1);
76.   cout << "计数:" << m_sort1.get_count() << endl;
77.   print_vector_int(V);
78. 
79.   MySort<int> m_sort2; //MySort有私有变量m_count,所以需要提供构造函数初始化m_count
80.   m_sort2 = for_each(V.begin(), V.end(), m_sort2);
81.   cout << "计数:" << m_sort2.get_count() << endl;
82.   print_vector_int(V);
83. 
84.   for_each(V.begin(), V.end(), my_add<int>);
85.   print_vector_int(V);
86. 
87.   system("pause");
88.   return 0;
89. }

总结

学习STL最好的教材就是源码,通过分析源码可以深刻理解STL容器、迭代器和算法的精髓。参考源码,才能写出我们自己的、编译器认可的、规范的代码。


相关文章
|
19天前
|
搜索推荐 Python
利用Python内置函数实现的冒泡排序算法
在上述代码中,`bubble_sort` 函数接受一个列表 `arr` 作为输入。通过两层循环,外层循环控制排序的轮数,内层循环用于比较相邻的元素并进行交换。如果前一个元素大于后一个元素,就将它们交换位置。
123 67
|
1月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
91 8
|
1月前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
97 7
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
76 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
3月前
|
XML JavaScript 前端开发
学习react基础(1)_虚拟dom、diff算法、函数和class创建组件
本文介绍了React的核心概念,包括虚拟DOM、Diff算法以及如何通过函数和类创建React组件。
33 3
|
2月前
|
算法 数据处理 C++
c++ STL划分算法;partition()、partition_copy()、stable_partition()、partition_point()详解
这些算法是C++ STL中处理和组织数据的强大工具,能够高效地实现复杂的数据处理逻辑。理解它们的差异和应用场景,将有助于编写更加高效和清晰的C++代码。
38 0
|
2月前
|
存储 算法 安全
ArrayList简介及使用全方位手把手教学(带源码),用ArrayList实现洗牌算法,3个人轮流拿牌(带全部源码)
文章全面介绍了Java中ArrayList的使用方法,包括其构造方法、常见操作、遍历方式、扩容机制,并展示了如何使用ArrayList实现洗牌算法的实例。
24 0
|
4月前
|
JSON 算法 API
京东以图搜图功能API接口调用算法源码python
京东图搜接口是一款强大工具,通过上传图片即可搜索京东平台上的商品。适合电商平台、比价应用及需商品识别服务的场景。使用前需了解接口功能并注册开发者账号获取Key和Secret;准备好图片的Base64编码和AppKey;生成安全签名后,利用HTTP客户端发送POST请求至接口URL;最后解析JSON响应数据以获取商品信息。
|
4月前
|
数据采集 机器学习/深度学习 算法
【python】python客户信息审计风险决策树算法分类预测(源码+数据集+论文)【独一无二】
【python】python客户信息审计风险决策树算法分类预测(源码+数据集+论文)【独一无二】
|
4月前
|
算法
【Azure Developer】完成算法第4版书中,第一节基础编码中的数组函数 histogrm()
【Azure Developer】完成算法第4版书中,第一节基础编码中的数组函数 histogrm()