Effective STL
- 1. 容器
- 条例01:慎重选择容器类型
- 条例02:不要试图编写独立于容器类型的代码
- 条例03:确保容器中对象的拷贝正确而高效
- 条例04:调用empty而不是检查size()是否为空
- 条例05:区间成员函数优先于与之对应的单元素成员函数
- 条例06:当心C++编译器最烦人的分析机制
- 条例07:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
- 条例08:切勿创建包含auto_ptr的容器对象
- 条例09:慎重选择删除元素的方法
- 条例10:了解分配子的限制与约定
- 条例11:理解自定义分配子的合理用法
- 条例12:切勿对STL容器的线程安全性有不切实际的依赖
- 2. vector和string
- 条例13:vector和string优先于动态分配的数组
- 条例14:使用reserve来避免不必要的重新分配
- 条例15:注意string实现的多样性
- 条例16:了解如何把vector和string数据传给旧的AP1
- 条例17:使用“swap技巧”除去多余的容量
- 条例18:避免使用vector
- 3. 关联容器
- 条例19:理解相等(equality)和等价(equivalence)的区别
- 条例20:为包含指针类型的关联容器指定比较类型
- 条例21:总是让比较函数在等值情况下返回false
- 条例22:切勿直接修改set或者multiset中的键
- 条例23:考虑用排序的vector代替关联容器
- 条例24:当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择
- 条例25:熟悉非标准的散列容器
- 4. 迭代器
- 条例26:iterator优先于const_iterator、reverse_iterator及const_reverse_iterator (在C++11中并不使用)
- 条例27:使用distance和advance将容器的const_iterator转换成iterator
- 条例28:正确理解由reverse_iterator的base()成员函数所产生的iterator的用法
- 条例29:对于逐个字符的输入请考虑使用istreambuf_iterator
- 5. 算法
- 条例30:确保目标区间足够大
- 条例31:了解各种与排序有关的选择
- 条例32:如果确实要删除元素,则需要在remove这一类算法之后调用erase
- 条例33:对包含指针的容器使用remove这一类算法时要特别小心
- 条例34:了解哪些算法要求使用排序的区间作为参数
- 第35条:通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较
- 第36条:理解copy_if算法的正确实现
- 第37条:使用accumulate或者for_each进行区间统计
- 6. 函数子、函数子类、函数及其他
- 第38条:遵循按值传递的原则来设计函数子类
- 第39条:确保判别式是“纯函数”
- 第40条:若一个类是仿函数(函数对象),则应使它可适配
1. 容器
条例01:慎重选择容器类型
如何在vector、list、deque中做出选择:
- vector是默认应使用的序列类型;
- 当需要频繁的在序列中将做插入和删除操作时,应使用list。
- 当大多数插入和删除操作发生在序列的头部和尾部时,deque是应该考虑的数据结构。
连续内存容器:把它的元素存放在一块或者多块内存中,每块内存中存多个元素。但有新元素插入或者已有的元素被删除时,同一内存块中的其他元素要向前或者向后移动,以便为新元素让出空间,或者填充被删除元素留下的空隙。这种移动影响到效率和异常安全性。
标准的连续内存容器包括:vector、string、deque。
基于节点的容器:在每一个内存块中只存放一个元素。容器中元素的插入或者删除只影响到指向节点的指针,而不影响节点本身的内容,所以当有插入或者删除操作时,元素的值不需要移动。
基于节点的容器:list、所有标准的关联容器。
对插入和删除操作,如果需要事务语义(也就是说,在插入和删除操作失败时,需要回滚的能力),你就要使用基于节点的容器。如果对多个元素的插入操作需要事务语义,则你需要选择list,因为在标准容器中,只有list对多个元素的插入操作提供了事务语义。对那些希望编写异常安全代码的程序员,事务语义显得尤为重要。
string是STL中在swap过程中会导致迭代器、指针和引用变为无效的唯一容器。
条例02:不要试图编写独立于容器类型的代码
STL是以泛化原则为基础的:数组被泛化为“以其包含的对象类型为参数”的容器;函数被泛化为“以其使用的迭代器的类型为参数”的算法;指针被泛化为“以其指向的对象的类型为参数”的迭代器。
很多程序员想以一种不同的方式做泛化。他们在自己的软件中不是针对某种具体的容器,而是想把容器的概念泛化,这样他们就能使用,比如vector,而仍保留以后将其换成deque或list的选择——但不必改变使用该容器的代码。这种想法是不正确的!!!
试图编写独立于容器类型的代码——是不正确的!
条例03:确保容器中对象的拷贝正确而高效
容器中保存了对象,但并不是你提供给容器的那些对象。当你通过insert或者push_back向容器中加入对象时,存入容器的是你所指定的对象的拷贝。
如果向容器中填充对象,而对象的拷贝又非常耗时,那么向容器中填充对象这一操作可能成为程序的性能瓶颈。
在继承体系下,拷贝动作可能导致剥离(slicing)。也就是说,如果你创建一个存放基类对象的容器,却向其中插入派生类对象,那么派生类对象被拷贝进容器时,它所有的特性部分将被丢失。
剥离问题意味着向基类对象容器中插入派生类对象几乎总是错的。
使拷贝动作高效、正确并防止剥离问题发生的一个简单办法就是使容器包含指针而非对象。
条例04:调用empty而不是检查size()是否为空
empty对所有标准容器都是常数时间操作,而对于一些list实现,size()耗费线性时间。
条例05:区间成员函数优先于与之对应的单元素成员函数
给定v1和v2两个向量(vector),使v1的内容和v2的后半部分相同的最简单操作是什么?
v1.assign(v2.begin()+v2.size()/2 , v2.end());
使用区间成员函数的优点:
- 通过使用区间成员函数,通常可以少写一些代码。
- 使用区间成员函数通常可以得到意图清晰或更加直接的代码
当处理标准序列容器时,为了取得同样的结果,使用单元素的成员函数比使用区间成员函数需要更多地调用内存分配子,更频繁地拷贝对象,而且/或者做冗余的操作。
条例06:当心C++编译器最烦人的分析机制
class Widget{...};
Widget w();
此处并没有声明一个名为w的Widget类型变量,而是声明了一个名为w的函数,该函数不带任何参数。
C++中的一条普遍规律相符,即尽可能地解释为函数声明。
条例07:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
指针容器在自己被析构时会析构掉包含的每一个元素,当指针的析构函数什么都不做,当然也不会调用delete。
从没有析构函数的类进行共有继承是C++中的一项重要禁忌。
STL容器很智能,但没有智能到知道是否该删除自己所包含的指针的程度。当你使用指针的容器,而其中的指针应该被删除时,为了避免资源泄漏,你必须或者用引用计数形式的智能指针对象(比如Boost的shared_ptr)代替指针,或者当容器被析构时手工删除其中的每个指针。
条例08:切勿创建包含auto_ptr的容器对象
auto_ptr的容器(简称COAP)是被禁止的。
条例09:慎重选择删除元素的方法
对于删除特定元素:
对于连续内容容器(vector、deque、string)——使用erase-remove的方式进行删除元素
c.erase(remove(c.begin(),c.end(), 1963), c.end());
对于list,直接调用remove()接口进行元素删除。
对于标准关联容器,使用erase()接口删除元素。只需要对数时间开销。
关联容器的erase函数是基于等价而不是相等。
对于删除符合条件的元素:
对于连续内容容器(vector、deque、string)——使用erase-remove_if的方式进行删除元素
c.erase(remove_if(c.begin(),c.end(), Func), c.end());
对于list,直接调用remove_if()接口进行元素删除。
对于标准关联容器:有两种方式可以解决问题。
- 利用remove_copy_if把我们需要的值复制到一个新容器中,然后把原来容器的内容和新容器的内容相互交换。
- for循环中调用容器的erase()接口函数。
for(auto iter = c.begin(); iter!=c.end();)
{if(func(*iter)){c.erase(iter++);}else{iter++;}
}
对于标准序列容器,使用for进行元素删除时,一定要利用erase()函数的返回值。
for(auto iter = c.begin(); iter!=c.end();)
{if(func(*iter)){iter = c.erase(iter);}else{iter++;}
}
对于标准序列容器,一旦erase完成,其返回值指向被删除元素的下一个元素。
条例10:了解分配子的限制与约定
条例11:理解自定义分配子的合理用法
条例12:切勿对STL容器的线程安全性有不切实际的依赖
当涉及STL容器和线程安全性时,你可以指望一个STL库允许多个线程同时读一个容器,以及多个线程对不同的容器做写入操作。你不能指望STL库会把你从手工同步控制中解脱出来,而且你不能依赖于任何线程支持。
2. vector和string
条例13:vector和string优先于动态分配的数组
当决定使用new来动态分配内存时,将要承担以下责任:
- 确保以后会有人用delete来删除所分配的内存。
- 确保使用了正确的delete形式。
- 确保只delete一次。
当需要动态申请一个数组时,应该考虑使用vector或者string来代替。
条例14:使用reserve来避免不必要的重新分配
对于vector和string,每当需要更多的空间时,其操作分为四步:
- 分配一块大小为当前容器的某个倍数的新内存。大多数实现中都已2倍增长。
- 把容器中所有内存都从旧的内存中复制到新的内存中。
- 析构掉旧内存中的对象。
- 释放旧内存。
每当vector或者string扩容之后,所有指针、迭代器和引用都会变得无效。
reserve成员函数能使重新分配内存的次数减少到最低限度,从而避免了重新分配和指针、迭代器和引用失效带来的开销。
vector和string提供了4个函数:
- size():显示容器中有多少元素,但是不会告诉你该容器申请的内存数量。
- capacity():当前容器已经分配的内容可以容纳多少个元素。即容器的容量。
- resize():强迫容器改变到包含n个元素的状态。调用resize()之后,size()将返回n。如果n比当前的大小(size)要小,则容器尾部的元素将会被析构。如果n比当前的大小要大,则通过默认构造函数创建的新元素将被添加到容器的末尾。如果n比当前的容量要大,那么在添加元素之前,将先重新分配内存
- reserve():强迫容器将其容量改为n,前提是n不小于当前的大小。如果n比当前的容量小,则vector忽略该调用,什么也不做;而string则可能把自己的容量减为size()和n中的最大值,但是string的大小肯定保持不变
通常有两种方式来使用reserve以避免不必要的重新分配:
- 第一种方式是,若能确切知道或大致预计容器中最终会有多少元素,则此时可使用reserve。在这种情况下,就像上面代码中的vector一样,你可以简单地预留适当大小的空间。
- 第二种方式是,先预留足够大的空间(根据你的需要而定),然后,当把所有数据都加入以后,再去除多余的容量。
条例15:注意string实现的多样性
string的实现比乍看上去有更多的自由度;同样明显的是,不同的实现以不同的方式利用了这种设计上的灵活性。这些区别总结如下:
- string的值可能会被引用计数,也可能不会。
- string对象大小的范围可以是一个char*指针的大小的1倍到7倍。
- 创建一个新的字符串值可能需要零次、一次或两次动态分配内存。
- string对象可能共享,也可能不共享其大小和容量信息。
- string可能支持,也可能不支持针对单个对象的分配子。
- 不同的实现对字符内存的最小分配单位有不同的策略。
条例16:了解如何把vector和string数据传给旧的AP1
针对vector:使用&v[0]作为指向第一个元素的指针。从而用来适配C语言接口。但是注意要先判空。
if(not v.empty())
{doSomething(&v[0], v.size());
}
当你需要一个指向vector中的数据的指针时,永远不应该使用begin()。如果为了某种原因决定用v.begin(),那么请使用&*v.begin(),因为这和&v[0]产生同样的指针。
针对string:使用c_str()成员函数返回的结果传递给C语言接口。即便字符串的长度为0,这种方式也没问题。
对于vector,如果你传递的C API改变了v中元素值的话,通常是没有问题的,但被调用的函数不能试图改变向量中元素的个数。
条例17:使用“swap技巧”除去多余的容量
使用swap方式将vector或者string中去除多余的容量:
vector<T>(v).swap(v);
这一技术并不保证一定能除去多余的容量。它意味着“在容器当前的大小确定的情况下,使容量在该实现下变为最小”。
swap技巧的一种变化形式可以用来清除一个容器,并使其容量变为该实现下的最小值。
vector<T>().swap(v);
vector的clear()不会改变vector的容量,只会清空其中的元素。
在做swap的时候,不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换(string除外)。在swap发生后,原先指向某容器中元素的迭代器、指针和引用依然有效,并指向同样的元素——但是,这些元素已经在另一个容器中了。
条例18:避免使用vector
作为一个STL容器,vector只有两点不对。首先,它不是一个STL容器。其次,它并不存储bool。除此以外,一切正常。
3. 关联容器
条例19:理解相等(equality)和等价(equivalence)的区别
STL中不同的函数对于“相同”的定义是不同的。find算法对相同的定义是“相等”,是以operator==为基础的;set::insert()对相同的定义是“等价”,是以operator<为基础的。
相等:的概念是基于operator==的。
等价:的概念是以“在已排序的区间中对象值的相对顺序”为基础的。
对于两个对象x和y,如果按照关联容器c的排列顺序,每一个都不在另一个的前面,那么称这两个对象按照c的排列顺序有 等价 的值。
使用单一的比较函数,并把等价关系作为判定两个元素是否“相同”的依据,使得标准关联容器避免了一大堆“若使用两个比较函数将带来的问题”。
条例20:为包含指针类型的关联容器指定比较类型
条例21:总是让比较函数在等值情况下返回false
对于关联容器,判断两个元素是否相同是通过等价来判断的,而等价的判断条件是 A不大于B 且 B 不大于A
,如果比较函数在等值的情况下返回true,则会导致等价条件判断错误。从而使关联容器发生错误。
条例22:切勿直接修改set或者multiset中的键
set或者multiset中的值不能是const的,因为将其定义为const并不合理。
set或者multiset中的键指的是用于判断小于
所使用的部分,一定不能修改set或者multiset中的键,因为这部分心意会影响容器的排序性。
所以,试图修改set或multiset中元素的代码将是不可移植的。因为不同厂商是set或者multiset使用的迭代器类型可能不能。
为了使修改set或者multiset中的元素可移植,可以使用强制类型转换将迭代器解引用并转换成引用类型
。注意此处必须转换成引用类型,因为转换成非引用类型会创建新的对象。
条例23:考虑用排序的vector代替关联容器
在排序的vector中存储数据可能比在标准关联容器中存储同样的数据要耗费更少的内存,而考虑到页面错误的因素,通过二分搜索法来查找一个排序的vector可能比查找一个标准关联容器要更快一些。
查找操作几乎从不跟插入和删除操作混在一起”时,再考虑使用排序的vector而不是关联容器才是合理的。
所以,当使用vector来模仿map<K,V>时,存储在vector中的数据必须是pair<K,V>,而不是pair<const K,V>。
条例24:当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择
map::operator[]的设计目的是为了提供“添加和更新”(add or update)的功能。
具体的工作方式是这样的,operator[] 返回一个引用,它指向与k相关联的值对象。然后v被赋给该引用(operator[]返回的那个引用)所指向的对象。
当向映射表中添加元素时,要优先选用insert而不是operator[] ;当更新已经在映射表中的元素的值时,要优先选择operator[]。
条例25:熟悉非标准的散列容器
4. 迭代器
条例26:iterator优先于const_iterator、reverse_iterator及const_reverse_iterator (在C++11中并不使用)
对容器类container而言,iterator类型的功效相当于T*,而const_iterator则相当于const T*。
reverse_iterator与const_reverse_iterator同样分别对应于T*和const T*,所不同的是,对这两个迭代器进行递增的效果是由容器的尾部反向遍历到容器头部。
C++98中容器的insert()和erase()的参数类型都是iterator类型,但是C++11中insert()和erase()的参数类型全都改成了const_iterator类型。
条例27:使用distance和advance将容器的const_iterator转换成iterator
对于这些容器类型,iterator和const_iterator是完全不同的类。试图将一种类型转换为另一种类型是毫无意义的,这就是const_cast转换被拒绝的原因。
条例28:正确理解由reverse_iterator的base()成员函数所产生的iterator的用法
通过base()函数可以得到一个与reverse_iterator“相对应的”iterator的说法并不准确。reverse_interator::base()得到的iterator指向的元素是reverse_interator指向元素的下一个元素。
条例29:对于逐个字符的输入请考虑使用istreambuf_iterator
5. 算法
条例30:确保目标区间足够大
无论何时,如果所使用的算法需要指定一个目标区间,那么必须确保目标区间足够大,或者确保它会随着算法的运行而增大。
要在算法执行过程中增大目标区间,请使用插入型迭代器,比如ostream_iterator,或者由back_inserter、front_inserter和inserter返回的迭代器。
条例31:了解各种与排序有关的选择
如果需要对vector、string、deque或者数组中的元素执行一次完全排序,那么可以使用sort或者stable_sort。
如果有一个vector、string、deque或者数组,并且只需要对等价性最前面的n个元素进行排序,那么可以使用partial_sort。
如果有一个vector、string、deque或者数组,并且需要找到第n个位置上的元素,或者,需要找到等价性最前面的n个元素但又不必对这n个元素进行排序,那么,nth_element正是你所需要的函数。
如果需要将一个标准序列容器中的元素按照是否满足某个特定的条件区分开来,那么,partition和stable_partition可能正是你所需要的。
如果你的数据在一个list中,那么你仍然可以直接调用partition和stable_partition算法;可以用list::sort来替代sort和stable_sort算法。但是,如果你需要获得partial_sort或nth_element算法的效果,那么,你可以有一些间接的途径来完成这项任务。
条例32:如果确实要删除元素,则需要在remove这一类算法之后调用erase
因为从容器中删除元素的唯一方法是调用该容器的成员函数,而remove并不知道它操作的元素所在的容器,所以remove不可能从容器中删除元素。
用remove从容器中删除元素,而容器中的元素数目却不会因此而减少。
remove不是真正意义上的删除,因为它做不到。
remove移动了区间中的元素,其结果是,“不用被删除”的元素移到了区间的前部(保持原来的相对顺序)。它返回的一个迭代器指向最后一个“不用被删除”的元素之后的元素。这个返回值相当于该区间“新的逻辑结尾”。
如果你真想删除元素,那就必须在remove之后使用erase。
vector<int> vv.erase(remove(v.begin(),v.end(),2) , v.end());
对于list,可以直接调用其remove成员函数。
条例33:对包含指针的容器使用remove这一类算法时要特别小心
但容器中存储指针时,在调用remove时就可能造成内存泄漏。
如果容器中存放的不是普通指针,而是具有引用计数功能的智能指针,那么与remove相关的困难就不再存在了。
条例34:了解哪些算法要求使用排序的区间作为参数
有些算法要求排序的区间,即区间中的值是排过序的。当使用这些算法的时候,遵循这条规则尤为重要,因为违反这一规则并不会导致编译器错误,而会导致运行时的未确定行为。
第35条:通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较
第36条:理解copy_if算法的正确实现
目前stl支持copy_if
第37条:使用accumulate或者for_each进行区间统计
使用accumulate统计容器中所有字符串的长度。
6. 函数子、函数子类、函数及其他
第38条:遵循按值传递的原则来设计函数子类
由于函数对象往往会按值传递和返回,所以,你必须确保你编写的函数对象在经过了传递之后还能正常工作。这意味着两件事:首先,你的函数对象必须尽可能地小,否则复制的开销会非常昂贵;其次,函数对象必须是单态的(不是多态的),也就是说,它们不得使用虚函数。
第39条:确保判别式是“纯函数”
一个判别式(predicate)是一个返回值为bool类型(或者可以隐式地转换为bool类型)的函数。
一个纯函数(pure function)是指返回值仅仅依赖于其参数的函数。
在C++中,纯函数所能访问的数据应该仅局限于参数及常量(在函数生命期内不会被改变,自然地,这样的常量数据应该被声明为const)。如果一个纯函数需要访问那些可能会在两次调用之间发生变化的数据,那么用相同的参数在不同的时刻调用该函数就有可能会得到不同的结果,这将与纯函数的定义相矛盾。