前言
在STL容器中有一种集合容器set,set容器内部元素唯一并按照一定规则顺序排列。下面将通过set容器装入基本数据类型和类对象的实例,在程序中一步步分析函数对象的本质和使用方法,并延伸出自己在学习STL时的一些心得体会。
话题引出:set容器自动排序的实现
我们首先定义一个int类型的set容器,并放入int类型数据
1. #define _CRT_SECURE_NO_WARNINGS 2. #include <iostream> 3. using namespace std; 4. 5. #include <set> 6. 7. //存入基本数据类型 8. void FuncTest1() 9. { 10. set<int, less<int>> s1; 11. 12. int temp; 13. cout << "输入set的值:"; 14. cin >> temp; 15. while (temp != EOF) 16. { 17. s1.insert(temp); 18. cout << "输入set的值:"; 19. cin >> temp; 20. } 21. 22. for (set<int, less<int>>::iterator it = s1.begin(); it != s1.end(); it++) 23. { 24. cout << *it << " "; 25. } 26. cout << endl; 27. 28. cout << "s1长度:" << s1.size() << endl; 29. 30. while (!s1.empty()) 31. { 32. cout << *s1.begin() << " "; 33. s1.erase(s1.begin()); 34. } 35. cout << endl; 36. 37. s1.clear(); 38. cout << "s1长度:" << s1.size() << endl; 39. } 40. 41. int main() 42. { 43. FuncTest1(); 44. 45. system("pause"); 46. return 0; 47. }
通过程序运行结果,我们可以初步了解到set容器元素唯一和自动排序的特性。那么有一个问题,set自动排序的规则是怎么确定的,这个规则是否可以由我们自己制定呢?
我们默认情况下定义一个容器
set<int> s;
它实际上是
set<int, less<int>> s;
这里的less就是一个函数对象,它规定了这个set容器内部排序规则为从小到大排序,实际上在set容器种装入元素时,他会首先进入less这个函数,满足规则才放入元素并根据规则存放,不满规则(重复数据)就放入失败,这个在后面会说。
这样,在set容器放入int型数据,实现自动排序的机制我们就了解了,他是采用了一个类似于回调函数的方式实现的。那么新的问题又出现了,当我放入一个int型数据的时候,set默认知道如何排序,那我们放入一个既有int数据又有字符串以及更复杂的数据类型时,set能否自动排序呢?我们通过下面的例子分析。
set容器装入类对象元素
首先我们定义一个类MyString
1. class MyString 2. { 3. public: 4. MyString(const char* str) 5. { 6. if (str == NULL) 7. { 8. throw "str == NULL"; 9. } 10. this->len = strlen(str); 11. this->str = new char[this->len + 1]; 12. strcpy(this->str, str); 13. } 14. MyString(const MyString& s) //必须加const 15. { 16. this->len = s.len; 17. this->str = new char[this->len + 1]; 18. strcpy(this->str, s.str); 19. } 20. ~MyString() 21. { 22. if (this->str != NULL) 23. { 24. delete[] this->str; 25. this->str = NULL; 26. } 27. this->len = 0; 28. } 29. public: 30. friend ostream& operator<<(ostream& out, MyString s); 31. public: 32. int len; 33. char* str; 34. }; 35. 36. ostream& operator<<(ostream& out, MyString s) 37. { 38. out << "string: " << s.str << "\t" << "strlen: " << s.len << endl; 39. return out; 40. }
这个类中包含两个成员,长度和字符串内容。这里需要注意一点,因为容器执行的是值拷贝,也就是把一个对象元素拷贝到容器中,所以在类中必须自定义一个拷贝构造函数,以免出现浅拷贝问题,并且装入set容器中的数据必须可以被拷贝(含有拷贝构造函数)。
定义好类后,我们在测试函数中放入类对象
1. void FuncTest2() 2. { 3. set<MyString> s; 4. 5. MyString s1("ccc"); 6. 7. s.insert(s1); 8. } 9. 10. int main() 11. { 12. system("pause"); 13. return 0; 14. }
编译程序,发现编译器报错
我们双击错误提示定义到错误位置
我们发现错误被定位到了xstddef文件的第127行,这根本就不是我们自己的cpp文件,我们仔细看这段代码可以发现,原来这就是我们上面提到的
set<int, less<int>> s;
中的函数对象less的实现,我们可以猜测这应该STL的源码,分析这段源码可以看到,less函数有两个参数left和right,返回值是一个bool类型,该函数返回两个参数的比较结果,由此可以看出,这个比较规则并不能对我们的MyString类对象进行比较,因为他不知道是比较对象的len成员还是str成员。那么我们就可以自己定义一个函数对象来实现MyString类对象的比较规则,首先介绍函数对象的定义。
函数对象(仿函数)
在C语言中,我们可以通过函数指针做函数参数来实现C语言风格的回调函数,在回调函数中实现自己需要的回调行为,详细可见本人博客
以及
在C++中还提供了另一种实现回调函数的方法,就是函数对象,也叫仿函数。
函数对象就是一个重载了函数调用操作符的类所定义的对象 ,该类对象表现出类似于函数调用的一个行为,通过函数对象可以实现自定义的回调行为。总之,函数对象类似于函数的功能,其实质就是一个回调函数,会在适当时机进行调用,并执行预定义的规则。下面将通过自定义一个函数对象,一步一步的分析函数对象及调用时机。
自定义函数对象实现MyString对象的排序规则
STL提供的默认排序机制less并不能对我们的MyString类对象进行排序,也就是函数对象less提供的排序机制不知道我们自定义的数据该如何排序。初次接触,我们可能并不知道如何定义一个函数对象的接口,它的返回值,参数等。这时我们可以通过VS的转到定义功能来查看源码,源码是最好的书籍和实例。less函数对象的源码如下:
1. /*此为源码*/ 2. // STRUCT TEMPLATE less 3. template <class _Ty = void> 4. struct less { 5. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type; 6. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type; 7. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type; 8. 9. constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const { 10. return _Left < _Right; 11. } 12. };
我们可以模仿less实现一个简单的函数对象
1. struct StringFunc //可参考源码模仿 2. { 3. //重载函数调用操作符,实现string类的排序功能 4. constexpr bool operator()(const MyString& s1, const MyString& s2) const 5. { 6. if (s1.len < s2.len) 7. { 8. return true; 9. } 10. else 11. { 12. return false; 13. } 14. } 15. };
我们的规则是按照字符串的长度进行排序,使用我们自定义的函数对象定义一个容器
1. void FuncTest2() 2. { 3. set<MyString, StringFunc> s; 4. 5. MyString s1("ccc"); 6. MyString s2("a"); 7. MyString s3("dddd"); 8. MyString s4("bb"); 9. MyString s5("eeeee"); 10. MyString s6("e"); 11. 12. s.insert(s1); 13. s.insert(s2); 14. s.insert(s3); 15. s.insert(s4); 16. s.insert(s5); 17. s.insert(s6); 18. 19. for (set<MyString, StringFunc>::iterator it = s.begin(); it != s.end(); it++) 20. { 21. cout << *it; 22. } 23. }
编译运行
我们看到打印结果,字符串按照长度进行了顺序排列,且长度相同的元素s6没有插入到容器中。这时候,我们又发现一个新问题,s6是没有插入还是插入后被覆盖了,又或者是其他什么情况导致他没有进入容器,那么我们定义的StringFunc是在什么时候调用的呢?
insert函数返回值、pair类型、以及函数对象调用时机
点击insert,转到定义,可以看到insert函数的原型
1. /*此为源码*/ 2. template <bool _Multi2 = _Multi, enable_if_t<!_Multi2, int> = 0> 3. pair<iterator, bool> insert(const value_type& _Val) { 4. const auto _Result = _Emplace(_Val); 5. return {iterator(_Result.first, _Get_scary()), _Result.second}; 6. }
我们看到insert这个函数是有返回值的,它返回一个pair<iterator, bool>。pair是一个对组,它将两个值放在一起当作一个单位使用,两个值可以是相同类型,也可以是不同类型。这里insert函数返回的一个是迭代器,一个是bool类型。我们可以通过pair的第二个值来判断插入是否成功。
我们把代码升级一下,通过一个pair变量来判断插入是否成功
1. void FuncTest2() 2. { 3. set<MyString, StringFunc> s; 4. 5. MyString s1("ccc"); 6. MyString s2("a"); 7. MyString s3("dddd"); 8. MyString s4("bb"); 9. MyString s5("eeeee"); 10. MyString s6("e"); 11. 12. s.insert(s1); 13. pair<set<MyString, StringFunc>::iterator, bool> my_pair; 14. my_pair = s.insert(s2); 15. if (my_pair.second == true) 16. { 17. cout << "insert success" << "\t" << *(my_pair.first); 18. } 19. else 20. { 21. cout << "insert failed" << endl; 22. } 23. s.insert(s3); 24. s.insert(s4); 25. s.insert(s5); 26. my_pair = s.insert(s6); 27. if (my_pair.second == true) 28. { 29. cout << "insert success" << "\t" << *(my_pair.first); 30. } 31. else 32. { 33. cout << "insert failed" << endl; 34. } 35. 36. for (set<MyString, StringFunc>::iterator it = s.begin(); it != s.end(); it++) 37. { 38. cout << *it; 39. } 40. }
我们在
my_pair = s.insert(s2);
处加一个断点,开始调试。通过单步调试可以看到,在插入元素前会先进入函数对象StringFunc所重载的()操作符函数中,如果插入成功,再去执行MyString的拷贝构造函数,把元素拷贝到容器中;如果返回false即容器中已经有同样长度(不符合规则)的元素,那么就不再执行拷贝构造函数复制元素了。
总结
学习STL遇到不认识的类型,不会的语法,可以通过VS的转到定义功能去查看源码。另外一定要多实践,把程序加断点一步一步的调试,观察程序每一步的行为,这样才能深刻理解C++程序和编译器的每一步行为。学习最重要的不是知识点本身,而是学习的方法和对待问题形成自己的理解。最后贴上完整程序:
1. #define _CRT_SECURE_NO_WARNINGS 2. #include <iostream> 3. using namespace std; 4. 5. #include <set> 6. 7. //存入基本数据类型 8. void FuncTest1() 9. { 10. set<int, less<int>> s1; 11. 12. int temp; 13. cout << "输入set的值:"; 14. cin >> temp; 15. while (temp != EOF) 16. { 17. s1.insert(temp); 18. cout << "输入set的值:"; 19. cin >> temp; 20. } 21. 22. for (set<int, less<int>>::iterator it = s1.begin(); it != s1.end(); it++) 23. { 24. cout << *it << " "; 25. } 26. cout << endl; 27. 28. cout << "s1长度:" << s1.size() << endl; 29. 30. while (!s1.empty()) 31. { 32. cout << *s1.begin() << " "; 33. s1.erase(s1.begin()); 34. } 35. cout << endl; 36. 37. s1.clear(); 38. cout << "s1长度:" << s1.size() << endl; 39. } 40. 41. class MyString 42. { 43. public: 44. MyString(const char* str) 45. { 46. if (str == NULL) 47. { 48. throw "str == NULL"; 49. } 50. this->len = strlen(str); 51. this->str = new char[this->len + 1]; 52. strcpy(this->str, str); 53. } 54. MyString(const MyString& s) //必须加const 55. { 56. this->len = s.len; 57. this->str = new char[this->len + 1]; 58. strcpy(this->str, s.str); 59. } 60. ~MyString() 61. { 62. if (this->str != NULL) 63. { 64. delete[] this->str; 65. this->str = NULL; 66. } 67. this->len = 0; 68. } 69. public: 70. friend ostream& operator<<(ostream& out, MyString s); 71. public: 72. int len; 73. char* str; 74. }; 75. 76. ostream& operator<<(ostream& out, MyString s) 77. { 78. out << "string: " << s.str << "\t" << "strlen: " << s.len << endl; 79. return out; 80. } 81. 82. //定义一个仿函数 83. /* 84. template <class _Ty = void> 85. struct less { 86. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type; 87. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type; 88. _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type; 89. 90. constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const { 91. return _Left < _Right; 92. } 93. }; 94. */ 95. struct StringFunc //可参考源码模仿 96. { 97. //重载函数调用操作符,实现string类的排序功能 98. constexpr bool operator()(const MyString& s1, const MyString& s2) const 99. { 100. if (s1.len < s2.len) 101. { 102. return true; 103. } 104. else 105. { 106. return false; 107. } 108. } 109. }; 110. 111. void FuncTest2() 112. { 113. set<MyString, StringFunc> s; 114. //set<MyString> s; //不知道该如何排序 115. //错误 C2676 二进制“ < ”:“const _Ty”不定义该运算符或到预定义运算符可接收的类型的转换 116. 117. MyString s1("ccc"); 118. MyString s2("a"); 119. MyString s3("dddd"); 120. MyString s4("bb"); 121. MyString s5("eeeee"); 122. MyString s6("e"); 123. 124. s.insert(s1); 125. /* 126. template <bool _Multi2 = _Multi, enable_if_t<!_Multi2, int> = 0> 127. pair<iterator, bool> insert(const value_type& _Val) { 128. const auto _Result = _Emplace(_Val); 129. return {iterator(_Result.first, _Get_scary()), _Result.second}; 130. } 131. */ 132. pair<set<MyString, StringFunc>::iterator, bool> my_pair; 133. my_pair = s.insert(s2); //先调用(),再调用拷贝构造函数 134. if (my_pair.second == true) 135. { 136. cout << "insert success" << "\t" << *(my_pair.first); 137. } 138. else 139. { 140. cout << "insert failed" << endl; 141. } 142. s.insert(s3); 143. s.insert(s4); 144. s.insert(s5); 145. my_pair = s.insert(s6); 146. if (my_pair.second == true) 147. { 148. cout << "insert success" << "\t" << *(my_pair.first); 149. } 150. else 151. { 152. cout << "insert failed" << endl; 153. } 154. 155. for (set<MyString, StringFunc>::iterator it = s.begin(); it != s.end(); it++) 156. { 157. cout << *it; 158. } 159. } 160. 161. int main() 162. { 163. //FuncTest1(); 164. FuncTest2(); 165. 166. system("pause"); 167. return 0; 168. }