第 30 条:确保目标区间足够大。
-
本条款讲解用的函数是
transform
函数。 -
transform
函数有两个重载函数,当前简介一个版本即可:transform(first, last, result, op)
。 -
transform
函数的具体实现如下:template <class InputIterator, class OutputIterator, class UnaryOperator> OutputIterator transform (InputIterator first1, InputIterator last1, OutputIterator result, UnaryOperator op) { while (first1 != last1) { *result = op(*first1); // or: *result=binary_op(*first1,*first2++); ++result; ++first1; } return result; }
-
transform
函数的作用是,将某操作(op
)应用于指定范围(first-last
)的每个元素,移动到result
来存放结果;op
操作会被调用last - first + 1
次,每次都会存放到result + i
的位置上(i
指的是第i
次被调用)。【返回的迭代器指向输出序列所保存的最后一个元素的下一个位置】
-
本条款的代码如下:
int transmogrify(int x) { return (x + 1); } int test_item_30() { std::vector<int> values{ 1, 2, 3 }; std::vector<int> results; // 第一种情况:back_inserter results.reserve(results.size() + values.size()); // 可避免内存的重新分配 //std::transform(values.cbegin(), values.cend(), results.end(), transmogrify); // 错误,segmentation fault std::transform(values.cbegin(), values.cend(), std::back_inserter(results), transmogrify); // 正确 // 在内部,std::back_inserter返回的迭代器将使得push_back被调用,所以back_inserter可适用于所有提供了push_back方法的容器 // 第二种情况:front_inserter std::list<int> results2; std::transform(values.begin(), values.end(), std::front_inserter(results2), transmogrify); // 前面这个语句插入的结果可能不如预期, // 例如,将values = [1,2,3]传到到results = [4,5,6]的前面 // 用前面的语句,结果会变成[3,2,1,4,5,6],所以需要将[1,2,3]反序插入 std::transform(values.cbegin(), values.cend(), std::front_inserter(results2), transmogrify); // std::front_inserter在内部利用了push_front,所以front_inserter仅适用于那些提供了push_front成员函数的容器 // 第三种情况:inserter【插入任意位置】 std::transform(values.begin(), values.end(),inserter(results, results.begin() + results.size()/2),transmogrify); // 把它们的结果插入容器中的任意位置 // 第四种情况:用resize而并非reverse if (results.size() < values.size()){ // 确保results至少和values一样大 results.resize(values.size()); } transform(values.begin(), values.end(), results.begin(), transmogrify); // 覆盖values.size()个 // 第五种情况:清空results然后用通常的方式使用插入迭代器 results.clear();// 销毁results中的所有元素 results.reserve(values.size()); // 保留足够空间 transform(values.begin(), values.end(), back_inserter(results), transmogrify); return 0; }
-
上面的代码有几点值得进行讨论:
- 在前面用
transform
错误操作的时候,result
迭代器参数位置传入的是results.end()
,所以在调用transform
的时候,就会给不存在的对象赋值。【第一种情况】- 而传入
back_inserter(results)
的时候,问题就能解决了;调用back_inserter
来产生指定目标区间起点的迭代器,在内部,back_inserter
返回的迭代器会调用push_back
。【插入后面的同时push_back
】
- 而传入
- 如果你想让一个算法在容器的前端插入东西,你可以使用
front_inserter
;在内部,front_inserter
利用了push_front
,所以front_insert
只和提供那个成员函数的容器配合(也就是deque
和list
)。【插入前面的同时push_front
】【第二种情况】 - 如果在
vector
和string
中,想要调用transform
,可以提前先调用reverse
来分配足够内存。【第一种情况】 - 如果不想用
reverse
:可以考虑保障results
至少有values
的元素个数,即使用resize
【第四种情况】;又或者先清空results
再用通常方式插入【第五种情况】。
- 在前面用
-
最后的总结如下:
- 无论何时你使用一个要求指定目的区间的算法,确保目的区间已经足够大或者在算法执行时可以增加大小。
- 如果选择增加大小,就使用插入迭代器,比如
ostream_iterators
或从back_inserter
、front_inserter
或inserter
返回的迭代器。
第 31 条:了解各种与排序有关的选择。
- 本条款讨论的就是在哪种情况下用哪种排序算法。
- stl 中
sort
相关函数及简介如下:std::sort
、std::stable_sort
和std::partial_sort
:std::sort
对给定区间所有元素进行排序,采用类似快速排序算法来实现;【非稳定排序】std::stable_sort
对给定区间所有元素进行稳定排序,采用类似归并算法实现;std::partial_sort
对给定区间所有元素进行部分排序,采用类似堆排序算法实现。【非稳定排序】
std::nth_element
:【与partial_sort
不同的点在于,partial_sort
的前半部分是完全排序的;nth_element
把前n
个值放在前部分,但是整体无序】【其实就是快速选择】std::nth_element
使得位置n
上的元素正好是全排序情况下的第n
个元素;前n-1
个比第n
个值小的值在n
位置之前,n
位置之后比第n
个值大的值在n
位置之后。
std::partition
和std::stable_partition
:【与nth_element
区别在于partition
自定义判断条件】【只需要双向迭代器,不需要随机访问】std::partition
会将区间[first,last)
中的元素重新排列,满足判断条件pred
的元素会被放在区间的前段,不满足pred
的元素会被放在区间的后段。【非稳定】std::stable_partition
保证初始相对位置。
第 32 条:如果确实需要删除元素,则需要在 remove 这一类算法之后调用 erase。
-
从容器中删除元素的唯一方法是调用该容器的成员函数,而
remove
并不知道它操作的元素所在的容器(remove
并不是成员函数,list
除外),所以remove
不可能从容器中删除元素。 -
remove
并没有做到真正的删除元素,只是把该删除的元素移动到容器最后,然后把尾部迭代器前移n
个(假设只删除n
个元素);【可以把remove
想象成一个压缩过程,被删除的值表演了在压缩中被填充的洞的角色】- 即
remove
返回的一个迭代器指向最后一个“不用被删除”的元素之后的元素,这个返回值相当于该区间“新的逻辑结尾”。
- 即
-
std::list
的remove
成员函数是 STL 中唯一一个名为remove
并且确实删除了容器中元素的函数。 -
remove()
函数并不是真正的删除,要想真正删除元素则可以使用erase()
或者resize()
函数。v.erase(std::remove(v.begin(), v.end(), 99), v.end()); // 真正删除所有值等于99的元素
-
std::remove
并不是唯一一个适用于这种情形的算法,其它还有两个属于remove
类的算法:remove_if
和unique
。remove_if
和remove
的情况一致,只不过remove_if
传入了判断式,满足判断式的才remove
。unique
的情况和remove
一致,相当于把重复值移到后面。- 而和
list::remove
一样,list::unique
会真的删除元素;它旨在删除临近的重复元素(使元素唯一),而且比使用erase
+unique
更为高效。
第 33 条:对包含指针的容器使用 remove 这一类算法时要特别小心。
- 当容器中存放的是指向动态分配的对象的指针的时候,应该避免使用
remove
和类似的算法(remove_if
和unique
)。- 引出的问题其实很直白:当调用
remove
函数移走指针元素后(此时已经造成资源泄露),再接着调用erase
函数真正删除元素,那么会造成资源泄露。
- 引出的问题其实很直白:当调用
- 消除资源泄露的做法:
- 将那些被
remove
走的元素一一删除(delete
)并置空。【先调用for_each
把满足条件的指针元素delete
,最后再erase
+remove
,过程中将指针置空】 - 存放引用计数功能的智能指针,那么就可以直接使用
erase
+remove
的习惯用法。【要知道的是,因为存放的是智能指针,要使erase
+remove
生效,需要保证智能指针能够隐式转换为智能指针内元素的指针】
- 将那些被
第 34 条:了解哪些算法要求使用排序的区间作为参数。
- 并非所有的算法都可以应用于任何区间。
- 举例一:
remove
算法要求单向迭代器并且要求可以通过这些迭代器向容器中的对象赋值;remove
不能用于由输入迭代器指定的区间,也不适用于map
或multimap
,同样不适用于某些set
和multiset
的实现。
- 举例二:很多排序算法要求随机访问迭代器,所以对于
list
的元素不可能调用这些算法。
- 举例一:
- 要求排序区间的 STL 算法:
- 要求排序区间:
binaray_search
、lower_bound
、upper_bound
、equal_range
、set_union
、set_intersection
、set_difference
、set_symmetric_difference
、merge
、inplace_merge
和includes
。 - 并不一定要求(同样有效率),但通常情况下会与排序区间一起使用:
unique
和unique_copy
。
- 要求排序区间:
- 本条款讨论的内容如下:
binaray_search
、lower_bound
、upper_bound
和equal_range
用于查找,且它们用二分法(所以要求排序);当它们接受了随机访问迭代器,才保证对数时间的查找效率。set_union
、set_intersection
、set_difference
和set_symmetric_difference
用于set
的各类操作,这四个函数保证了线性时间(所以要求排序)。merge
和inplace_merge
实际上实现了合并和排序的联合操作,它们读入两个排序的区间,然后合并成一个新的排序区间,其中包含了原来两个区间中的所有元素;同样的,因为要求区间排序,所以保证了线性时间的承诺。includes
用来判断一个区间中的所有对象是否都在另一个区间中,它承诺线性时间的效率。unique
和unique_copy
即使对于未排序的区间也有很好的行为,unique
使用了与remove
类似的办法来删除区间中的元素,而并非真正意义上的删除。- 所有要求排序区间的算法(除
unique
、unique_copy
以外)都使用等价来判断两个对象是否相同,这是与关联容器相同的;unique
、unique_copy
在默认情况下使用相等来判断两个对象是否相同,当然你可以改变这种默认的行为,只需要给算法提供一个用于比较的谓词。
- 最后一个讨论的内容其实很简单:
- 假设
sort
函数和binary_search
函数共同使用在同一个容器上,如果sort
调用的时候使用了自定义的比较函数,那么binary_search
就必须要使用同样的比较函数。
- 假设
第 35 条:通过 mismatch 或 lexicographical_compare 实现简单的忽略大小写的字符串比较。
-
首先介绍条款中相关的几个函数:
mismatch
将标记出两个区间中第一个对应位置不同的位置;如果两个字符串长度不同,那么我们必须把短的字符串作为第一个区间传入。【类似strcmp
(返回一个负数、零或正数)】lexicographical_compare
是strcmp
的一个泛化版本,可以比较任何类型的值的区间;lexicographical_compare
可以接受一个判别式,由判别式来决定两个值是否满足一个用户自定义的准则。【类似operator
(返回true
或false
)】strcmp
和strcmpi
通常是被优化过的,它们在长字符串的处理上一般要比通用算法mismatch
和lexicographical_compare
快。
-
代码实现如下:
// 实现一:【忽略大小写string比较的函数】 // 第一个函数,比较字符 // 忽略大小写地比较字符c1和c2,如果c1<c2,返回-1;如果c1>c2,返回1;如果c1==c2,返回0 int ciCharCompare(char c1, char c2) { int lc1 = std::tolower(static_cast<unsigned char>(c1)); int lc2 = std::tolower(static_cast<unsigned char>(c2)); if (lc1 < lc2) return -1; if (lc1 > lc2) return 1; return 0; } // 第二个函数 // 用作边界检查,真正工作,即调用前面第一个函数 int ciStringCompareImpl(const std::string& s1, const std::string& s2) { typedef std::pair<std::string::const_iterator, std::string::const_iterator> PSCI; PSCI p = std::mismatch(s1.begin(), s1.end(), s2.begin(), std::not2(std::ptr_fun(ciCharCompare))); if (p.first == s1.end()) { // 如果为true,要么s1和s2相等,或者s1比s2短 if (p.second == s2.end()) return 0; else return -1; } return ciCharCompare(*p.first, *p.second); // 字符串之间的关系和这两个不匹配的字符之间的关系相同 } // 第三个函数,即string比较函数 int ciStringCompare(const std::string& s1, const std::string& s2) { // 把短的字符串作为第一个区间传入 if (s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2); else return -ciStringCompareImpl(s2, s1); } // 实现二: // 实现函数子类,方便后续lexicographical_compare调用 // 返回在忽略大小写的情况下,c1是否在c2之前 bool ciCharLess(char c1, char c2) { return std::tolower(static_cast<unsigned char>(c1)) < std::tolower(static_cast<unsigned char>(c2)); } bool ciStringCompare2(const std::string& s1, const std::string& s2) { return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess); } // 实现三: bool ciStringCompare3(const std::string& s1, const std::string& s2) { // 前提:不考虑国际化支持,也确定字符串中不会包含内嵌的空字符 return strcmp(s1.c_str(), s2.c_str()); } int test_item_35() { std::string str1{ "xxz" }, str2{ "xxx" }; fprintf(stdout, "str1:str2: %d\n", ciStringCompare(str1, str2)); fprintf(stdout, "str1:str2: %d\n", ciStringCompare2(str1, str2)); fprintf(stdout, "str1:str2: %d\n", ciStringCompare3(str1, str2)); return 0; }
第 36 条:理解 copy_if 算法的正确实现。
- 本条款讨论的其实就是如何实现
copy_if()
算法,C++11 已经添加了该函数。copy_if()
算法可以从源序列复制使谓词(判断式)返回true
的元素,所以可以把它看作一个过滤器。
- 条款中放出了
copy_if()
一个实现:template<typename InputIterator, // 一个copy_if的正确实现 typename OutputIterator, typename Predicate> OutputIterator copy_if(InputIterator begin, InputIterator end, OutputIterator destBegin, Predicate p) { while (begin != end) {// 逐字节拷贝 if (p(*begin)) *destBegin++ = *begin; ++begin; } return destBegin; }
copy_if()
函数的使用如下:int test_item_36() { std::vector<int> v1{ 1, 2, 3, 4, 5 }, v2(v1.size()); auto it = std::copy_if(v1.begin(), v1.end(), v2.begin(), [](int i) { return (i % 2 == 1); }); v2.resize(std::distance(v2.begin(), it)); for (const auto& v : v2) fprintf(stdout, "%d\n", v); // 1 3 5 return 0; }
第 37 条:使用 accumulate 或者 for_each 进行区间统计。
accumulate()
不存在于<algorithm>
,它和其他三个数值算法(inner_product()
、adjacent_difference()
和partial_sum()
)都在<numeric>
中。accumulate()
只要求输入迭代器,所以你可以使用std::istream_iterator
和std::istreambuf_iterator
。accumulate()
有两种形式:- 第一种形式有两个迭代器和一个初始值,它返回该初始值加上由迭代器标识的区间中的值的总和;
- 第二种形式带两个迭代器、一个初始值和一个任意的统计函数。【会将前面三个参数传入统计函数】
- 有了
accumulate()
,不仅可以计算累加,也可以计算累乘(乘积的时候初值需要设置为1
)。-
但是,如果有更复杂的情况,
accumulate()
函数就很吃力,例如计算一个区间所有点的平均值; -
如果使用
accumulate()
函数,在此场景下仍能使用,但是它不符合 STL 标准,即传给accumulate()
的函数不允许有副作用,而操作的过程中会带来副作用。 -
代码如下:
struct Point{ Point(double initX, double initY): x(initX), y(initY) {} double x, y; }; //定义传入accumulate的计算函数 class PointAverage : public binary_function<Point, Point, Point> {//此处的继承参见条款40 public: PointAverage() : xSum(0), ySum(0), numPoints(0){} const Point operator()(const Point& avgSoFar, const Point& p){ ++numPoints; xSum += p.x; ySum += p.y; return Point(xSum/numPoints, ySum/numPoints);//每次返回当前遍历元素的平均值 } private: size_t numPoints;//记录当前读取元素的总数 double xSum;//记录所有横坐标的总和 double ySum;//记录所有纵坐标的总和 }; //调用函数 list<Point> lp; Point avg = accumulate(lp.begin(), lp.end(), Point(0, 0), PointAverage());
-
for_each()
是另一个可被用来统计区间的算法,且它不受accumulate()
的那些限制。for_each()
也带两个参数:一个是区间(两个迭代器),另一个是函数(通常是函数对象)。- 对区间中的每个元素都要调用这个函数,但是,传给
for_each()
的这个函数只接收一个实参(即当前的区间元素)。【只会将第一个元素(即first
迭代器)传入函数中】 std::for_each()
和std::accumulate()
在两个方面有所不同:accumulate()
暗示着这个算法将会计算出一个区间的统计信息,而for_each()
听起来就好像是对一个区间的每个元素做一个操作。accumulate()
直接返回我们所要的统计结果,而for_each()
却返回一个函数对象,我们必须从这个函数对象中提取出我们所要的统计信息(可以保存状态)。
- 代码如下:
struct Point{ Point(double initX, double initY): x(initX), y(initY) {} double x, y; }; //定义传入accumulate的计算函数 class PointAverage : public unary_function<Point, void> {//此处继承的函数不同了 public: PointAverage() : xSum(0), ySum(0), numPoints(0){} void operator()(const Point& p){ ++numPoints; xSum += p.x; ySum += p.y; return Point(xSum/numPoints, ySum/numPoints);//每次返回当前遍历元素的平均值 } Point result() const{ return Point(xSum/numPoints, ySum/numPoints); } private: size_t numPoints;//记录当前读取元素的总数 double xSum;//记录所有横坐标的总和 double ySum;//记录所有纵坐标的总和 }; //调用函数 list<Point> lp; Point avg = for_each(lp.begin(), lp.end(), PointAverage()).result();// 取出返回值的状态