条款30:确保目标区间足够大
STL容器在被添加时(通过insert、push_front、push_back等)自动扩展它们自己来容纳新对象。但是标准STL并不总是为你进行区间的扩充——当你是用了错误的迭代器时,下面这段代码存在致命的错误:
int transmogrify(int x); // 这个函数从x
// 产生一些新值
vector<int> values;
... // 把数据放入values
vector<int> results; // 把transmogrify应用于
transform(values.begin(), values.end(), // values中的每个对象
results.end(), // 把这个返回的values附加到results的末尾,但是由于end不是一个有效的迭代器导致运行时错误
transmogrify);
正确的代码如下:
vector<int> results; // 把transmogrify应用于
transform(values.begin(), values.end(), // values中的每个对象,
back_inserter(results), // 在results的结尾
transmogrify); // 插入返回的values
back_inserter返回的迭代器会调用push_back,所以你可以在任何提供push_back的容器上使用
back_inserter(也就是任何标准序列容器:vector、string、deque和list)。如果你想让一个算法在容器的前端插入东西,你可以使用front_inserter。在内部,front_inserter利用了push_front,所以front_insert只和提供那个成员函数的容器配合(也就是deque和list)。
front_inserter让你强制算法在容器前端插入它们的结果,back_inserter让你告诉它们把结果放在容器后端,inserter允许你强制算法把它们的结果插入容器中的任意位置:
vector<int> values; // 同上
...
vector<int> results; // 同上,除了现在
... // 在调用transform前
// results已经有一些数据
transform(values.begin(), values.end(), // 把transmogrify的
inserter(results, results.begin() + results.size()/2), // 结果插入
transmogrify); // results的中间
条款32:如果你真的想删除东西的话就在类似remove的算法后接上erase
remove的实质是遍历整个区间,当遇到满足条件的元素时进行标记——removeIt,然后使用下一个不满足条件的元素将其覆盖,最后返回最后一个不满足条件的元素的下一个元素的迭代器。因此,remove后一个容器的size实际是不会改变的,如果要删除满足条件的元素,则应该对其使用erase,如下:
vector<int> v;
v.erase(remove(v.begin(), v.end(), 99), v.end());// 删除remove返回的迭代器为起点,end问终点的迭代器之间的所有元素
对于list,也可以使用同样的方法,但是list的remove方法更加高效(它没有erase,它的remove不是仅仅移动,它做了remove模板函数与vector的erase函数所做的一切, 到那时更加高效)。
条款33:提防在指针的容器上使用类似remove的算法
如上,remove并不删除元素,而是对元素进行覆盖。如果容器中存放的是指针——指向动态分配的内存。如果使用remove作用于一个存放这样的指针的迭代器,则会发生内存泄露(同样的,erase删除指针也不会释放资源)。最好的办法是使用智能指针,或者在使用remove之前,将要删除的指针释放并置为null,如下:
void delAndNullifyUncertified(Widget*& pWidget) // 如果*pWidget是一个
{ // 未通过检验Widget,
if (!pWidget->isCertified()) { // 删除指针
delete pWidget; // 并且设置它为空
pWidget = 0;
}
}
// 把所有指向未通过检验Widget的指针删除并且设置为空
for_each(v.begin(), v.end(), delAndNullifyUncertified);
// 从v中除去空指针0必须映射到一个指针,让C++可以正确地推出remove的第三个参数的类型
v.erase(remove(v.begin(), v.end(), static_cast<Widget*>(0)),v.end());
条款34:注意哪个算法需要有序区间
不是所有算法可以用于任意区间。比如,remove需要前向迭代器和可以通过这些迭代器赋值的能力。所以,它不能应用于由输入迭代器划分的区间,也不能是map或multimap,也不能是set和multiset的一些实现。同样,很多排序算法需要随机访问迭代器,所以不可能在一个list的元素上调用这些算法。如果你冒犯了这些规则,你的代码将不能编译,可能会出现一个非常冗长和不可理解的错误信息。但其他算法的需求则更为狡猾。在这些之中,可能最常见的就是一些算法需要有序值的区间。无论何时都应该坚持这个需求,因为冒犯它不仅会导致编译器诊断,而且会造成未定义的运行期行为。
以下为只能操作有序数据的算法的清单:
binary_search
lower_bound
upper_bound
equal_range
set_union
set_intersection
set_difference
set_symmetric_difference
merge
inplace_merge
includes
另外,下面的算法一般用于有序区间,虽然它们不要求:
unique unique_copy
搜索算法binary_search、lower_bound、upper_bound和equal_range需要有序区间,因为它们使用二分法查找来搜索值。像C库中的bsearch,这些算法保证了对数时间的查找,但作为交换的是,你必须给它们已经排过序的值。实际上,这些算法保证对数时间查找不是很正确。仅当传给它们的是随机访问迭代器时它们才能保证有那样的性能。如果给它们威力比较小的迭代器(比如双向迭代器),它们仍然进行对数次比较,但运行是线性时间的。
算法set_union、set_intersection、set_difference和set_symmetric_difference的四人组提供了线性时间设置它们名字所提出的操作的性能。为什么它们需要有序区间?因为如果不是的话,它们不能以线性时间完成它们的工作。如果你开始发觉一个趋势——需要有序区间的算法为了比它们用于可能无序区间提供更好的性能而这么做,你是对的。保持协调,这个趋势会继续。
merge和inplace_merge执行了有效的单遍合并排序算法:它们读取两个有序区间,然后产生一个包含了两个源区间所有元素的新有序区间。它们以线性时间执行,如果它们不知道源区间已经有序就不能完成。
最后一个需要有序区间的算法是includes。它用来检测是否一个区间的所有对象也在另一个区间中。因为includes可能假设它的两个区间都已经有序,所以它保证了线性时间性能。没有那个保证,一般来说它会变慢。
如果你要unique从一个区间去除所有重复值(也就是,让区间中所有值“唯一”),你必须先确保所有重复值一个接着一个——也就是需要区间有序。另外,unique从一个区间除去元素的方式和remove一样,也就是说它只是区分出不除去的元素。