50条有效使用STL的经验
- 第一条 慎重选择容器类型(20190713)
- 第二条 不要试图编写独立于容器类型的代码(20190713)
- 第三条 确保容器中的对象副本正确而高效(20190713)
- 第四条 调用empty而不是检查size()是否为0(20190714)
- 第五条 区间成员函数优先于与之对应的单元素成员函数(20190714)
- 第六条 当心C++编译器最烦人的分析机制(20190716)
- 第七条 如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉(20190717)
- 第八条 切勿创建包含auto_ptr的容器对象(20190717)
- 第九条 慎重选择删除元素的方法(20190718)
- 第十条 了解分配子(allocator)的约定和限制(20190721)
- 第十一条 理解自定义分配子的合理用法
- 第十二条 切勿对STL容器的线程安全性有不切实际的依赖
- 第十三条 vector和string优先于动态分配的数组(20190722)
- 第十四条 使用reserve来避免不必要的重新分配(20190722)
- 第十五条 注意string实现的多样性(20190723)
- 第十六条 了解如何把vector和string数据传给旧的API(20190723)
- 第十七条 使用"swap"技巧除去多余的容量(20190725)
- 第十八条 避免使用vector< bool>(20190725)
- 第十九条 理解相等(equality)和等价(equivalence)的区别(20190726)
- 第二十条 为包含指针的关联容器指定比较类型(20190726)
- 第二十一条 总是让比较函数在等值情况下返回false(20190728)
- 第二十二条 切勿直接修改set或multiset中的键(20190728)
- 第二十三条 考虑用排序的vector替代关联容器(20190729)
- 第二十四条 当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择(20190731)
- 第二十五条 熟悉非标准的散列容器(20190731)
- 第二十六条 iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator(20190802)
- 第二十七条 使用distance和advance将容器的const_iterator转换成iterator(20190802)
- 第二十八条 正确理解由reverse_iterator的base()成员函数所产生的iterator的用法(20190804)
- 第二十九条 对于逐个字符的输入请考虑使用istreambuf_iterator
- 第三十条 确保目标区间足够大(20190805)
- 第三十一条 了解各种与排序有关的选择(20190805)
- 第三十二条 如果确实需要删除元素,则需要在remove这一类算法之后调用erase(20190806)
- 第三十三条 对包含指针的容器使用remove这一类算法时要特别小心(20190806)
- 第三十四条 了解哪些算法要求使用排序的区间作为参数(20190806)
- 第三十五条 通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较(20190807)
- 第三十六条 理解copy_if算法的正确实现(20190807)
- 第三十七条 使用accumulate或者for_each进行区间统计(20190808)
- 第三十八条 遵循按值传递的原则来设计函数子类
- 第三十九条 确保判别式是“纯函数”
- 第四十条 若一个类是函数子,则应使它可配接
- 第四十一条 理解ptr_fun、men_fun和mem_fun_ref的来由
- 第四十二条 确保less< T >与operator<具有相同的语义
- 第四十三条 算法调用优先于手写的循环
- 第四十四条 容器的成员函数优先于同名的算法
- 第四十五条 正确区分count、find、binary_search、lower_bound、upper_bound和equal_range
- 第四十六条 考虑使用函数对象而不是函数最为STL算法的参数
- 第四十七条 避免产生“直写型”(write-only)的代码
- 第四十八条 总是包含(#include)正确的头文件
- 第四十九条 学会分析与STL相关的编译器诊断信息
- 第五十条 熟悉与STL相关的Web站点(20190808)
第一条 慎重选择容器类型(20190713)
除了上述几种,书中还提到:vector作为string的替代;vector作为标准关联容器的替代。
slist(单向链表)和rope(“重型”string)是SGI library中的扩展容器,使用它们需要包含头文件<ext/slist>、<ext/rope>,并使用命名空间__gnu_cxx。
unordered_set即原来的hash_set,其他同理。
标准的非STL容器指的是“可以认为它们是容器,但是它们并不满足STL容器的所有要求”。如stack有人说为“容器适配器”而不是“容器”。
连续内存容器(contiguous-memory container或称为基于数组的容器(array-based container)):元素存放在一块或多块(动态分配的)内存中,每块内存中存有多个元素。当有新元素插入或已有的元素被删除时,同一内存块的其他元素要向前或向后移动,以便为新元素让出空间,或者填充被删除元素所留下的空隙。这种移动影响到效率和异常安全性。标准的连续内存容器有vector、string和deque。非标准的rope也是一个连续内存容器。
基于节点的容器在每一个(动态分配的)内存块中只存放一个元素。容器中元素的插入和删除只影响到指向节点的指针,而不影响节点本身的内容,所以当有插入或删除操作时,元素的值不需要移动。表示链表的容器,如list和slist,是基于节点的;所有标准的关联容器也是如此(通常的实现方式是平衡树)。非标准的散列容器使用不同的基于节点的实现。
如何选择容器:
1、若需要在容器的任意位置插入新元素,选择序列容器;关联容器是不行的。
2、若关心容器中的元素是排列的,则散列容器是一个可行的选择;否则要避免散列容器。
3、若选择的容器必须是标准C++的一部分,就排除了散列容器、slist和rope。
4、若需要随机访问迭代器,则对容器的选择就被限定为vector、deque和string,也可以考虑rope;若需要双向迭代器,那么必须避免slist以及散列容器的一个常见实现(见第二十五条)。
5、发生元素的插入或删除操作时,若需要避免移动容器中原来的元素,则要避免连续内存的容器。
6、若容器中数据的布局需要和C兼容,则只能选择vector。
7、若元素的查找速度是关键的考虑因素,则要考虑散列容器(见第二十五条)、排序的vector(见第二十三条)和标准关联容器。
8、若介意容器内部使用了引用计数技术(reference counting),则要避免使用string和rope。当然,若需要某种表示字符串的方法,这是可以考虑vector。
9、对于插入和删除操作,若需要事务语义(transactional semantics)即在插入和删除操作失败时,若需要回滚的能力,则使用基于节点的容器。
10、若需要使迭代器、指针和引用变为无效的次数最少,则要使用基于节点的容器,因为对这类容器的插入和删除操作从来不会使迭代器、指针和引用变为无效(除非它们指向了一个你正在删除的元素)。而针对连续内存容器的插入和删除操作一般会使指向该容器的迭代器、指针和引用变为无效。
11、当插入操作仅在容器末尾发生时,deque的迭代器有可能会变为无效。deque是唯一的、迭代器可能会变为无效而指针和引用不会变为无效的STL标准容器。
第二条 不要试图编写独立于容器类型的代码(20190713)
STL是以泛化(generalization)原则为基础的:数组被泛化为“以其包含的对象的类型为参数”的容器;函数被泛化为“以其使用的迭代器的类型为参数”的算法;指针被泛化为“以其指向的对象的类型为参数”的迭代器。
标准的连续内存容器提供了随机访问迭代器,而标准的基于节点的容器提供了双向迭代器。
即便是最热心地倡导独立于容器类型的代码的人也很快会意识到,试图编写对序列容器和关联容器都适用的代码几乎是毫无意义的。
书中提到即使像insert和erase这样的操作,也会随容器类型的不同而表现出不同的原型和语义。比如,向序列容器中插入对象时,该对象位于被插入的位置处;而向关联容器中插入对象时,容器会按照其排序规则,将该对象移动到适当的位置处。又如,当带有一个迭代器参数的erase作用于序列容器时,会返回一个新的迭代器,而当它作用于关联容器时则没有返回值 。
但是显然,C++98关联容器的erase操作无返回值的形式在C++11已经会一样地返回一个迭代器了。
考虑到有时候不可避免地要从一种容器类型转到另一种,可以使用常规的方式来实现这种转变:使用封装(encapsulation)技术。最简单的方式是通过对容器类型和其迭代器类型使用类型定义(typedef)。因此,不要这样写:
class Widget{...};
vector<Widget> vw;
Widget bestWidget;
... //为bestWidget赋一个值
vector<Widget>::iterator i= //找到一个与bestWidget
find(vw.begin(),vw.end(),bestWidget) //具有同样的值的Widget
而要这样写:
class Widget{...};
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
Widget bestWidget;
...
WCIterator i =
find(cw.begin(),cw.end(),bestWidget);
这样就使得改变容器类型要容易的多,尤其当这种改变仅仅是增加一个自定义的分配子时,就显得更为方便。
第三条 确保容器中的对象副本正确而高效(20190713)
复制对象是STL的工作方式。进去容器的是副本,从容器出来的也是副本。
利用一个对象的复制成员函数可以很方便地复制该对象,特别是对象的复制构造函数和复制赋值操作符。对Widget这样的用户自定义类,这些函数通常被声明为:
class Widget
{
public:
...
Widget(const Widget&); //复制构造函数
Widget& operator=(const Widget&); //复制赋值操作符
...
};
如果向容器中填充对象,而对象的复制操作又很费时,那么向容器中填充对象这一简单的操作将会成为程序的性能“瓶颈”。
在存在继承关系的情况下,复制动作会导致剥离(slicing)。即如果创建了一个存放基类对象的容器,却向其中插入派生类的对象,那么在派生类对象(通过基类的复制构造函数)被复制进容器时,它所特有的部分(即派生类中的信息)将会丢失。
使复制动作高效、正确,并防止剥离问题发生的一个简单办法是使容器包含指针而不是对象。即使用Widget*的容器,而不是Widget的容器。当然,指针的容器也会有一些其他问题(参考第七条和第三十三条)。如果想避开这些问题,同时又想保证效率、正确性,避免剥离问题的话,智能指针(smart pointer)是一个诱人的选择。
第四条 调用empty而不是检查size()是否为0(20190714)
对任一容器c,c.size()==0本质上与c.empty()是等价的。但应该使用empty(),理由很简单:empty对所有的标准容器都是常数时间操作,而对一些list实现,size耗费线性时间。
显然,C++98中list的size()时间复杂度接近线性,但是C++11中list的size()时间复杂度是已经常数了。
不管发生了什么,调用empty而不是检查size==0是否成立总是没错的。所以,如果想知道容器中是否含有零个元素,请使用empty。
第五条 区间成员函数优先于与之对应的单元素成员函数(20190714)
假定要把一个int数组复制到一个vector的前端,使用vector的区间insert函数,非常简单:
int data[numValues];
vector<int> v;
...
v.insert(v.begin(),data,data+numValues);
而通过显示地循环调用insert,或多或少会这样:
vector<int>::iterator insertLoc(v.begin());
for(int i=0;i<numValues;++i)
{
insertLoc=v.insert(insertLoc,data[i]);
++insertLoc;
}
这里必须得将insert的返回值记下来供下次进入循环使用。如果在每次插入操作后不更新insertLoc,会遇到两个问题。首先,第一次迭代后的所有循环迭代都将导致不可预料的行为,因为每次调用insert都会使insertLoc无效。其次,即使insertLoc仍然有效,插入总是发生在vector的最前面(即在v.begin())处,结果这组整数倍以相反的顺序复制到v当中。
使用单元素版本的insert在三个方面影响了效率:1、影响不必要的函数调用;2、把v中已有的元素频繁地移动到插入后它们所处的位置;3、内存分配问题。
哪些成员函数支持区间:1、区间创建(构造函数);2、区间插入(insert);3、区间删除(erase);4、区间赋值(assign)。
优先选择区间成员函数而不是其对应的单元素成员函数有三条充分的理由:1、区间成员函数写起来更容易;2、更能清楚地表达意图;3、表现出了更高的效率。
第六条 当心C++编译器最烦人的分析机制(20190716)
int f(double d); //声明了一个带double参数并返回int的函数
int f(double (d));//同上;参数d两边的括号是多余的,会被忽略
int f(double) //同上;省略了参数名称
int g(double (*pf)());//声明了一个函数g,参数是一个指向不带任何参数的函数的指针,该函数返回double值
int g(double pf());//同上;pf为隐式指针
int g(double ());//同上;省去参数名
注意围绕参数名的括号与独立的括号的区别。围绕参数名的括号被忽略,而独立的括号则表明参数列表的存在。
假设有一个存有整数(int)的文件,你想把这些整数复制到一个list中。下面是很合理的一种做法:
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>());//小心!结果不会是你所想象的那样
这种做法的思路,是一对istream_iterator传入到list的区间构造函数中,从而把文件中的整数复制到list中。这段代码可以通过编译,但是运行的结果和想象的不一样。
list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>());
参考开头所说,这声明了一个函数data,其返回值是list< int>。这个data函数有两个参数:
1、第一个参数的名称是dataFile。它的类型是istream_iterator< int>。dataFile两边的括号是多余的,会被忽略。
2、第二个参数没有名称。它的类型是指向不带参数的函数的指针,该函数返回一个istream_iterator< int>。
C++的一条普遍规律:尽可能地解释为函数声明。
该规律的另一种表现形式:
class Widget{...};//假定Widget有默认构造函数
Widget w();//哦...
它没有声明名为w的Widget,而是声明了一个名为w的函数,该函数不带任何参数,并返回一个Widget。学会识别这一类言不达意是称为C++程序员的必经之路。
我们想用文件的内容初始化list< int>对象。现在我们已经知道必须绕过某一种分析机制:把形式参数的声明用括号括起来是非法的,但给函数参数加上括号却是合法的。所以通过增加一对括号,强迫编译器按我们的方式来工作:
list<int> data((istream_iterator<int>(dataFile)),istream_iterator<int>());//注意list构造函数的第一参数两边的括号
不幸的是,并不是所有的编译器都知道这一点。
我用DevC++测试是知道这一点的。
更好的方式是在对data的声明中避免使用匿名的istream_iterator对象(尽管使用匿名对象是一种趋势),而是给这些迭代器一个名称。
使用命名的迭代器对象与通常的STL程序风格相违背,但能使对所有编译器都没有二义性,并且使维护代码的人理解起来更容易。
第七条 如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉(20190717)
STL中的容器提供了迭代器,以便进行向后和向前的遍历(通过begin、end、rbegin等);提供了所包含的元素类型(通过value_type类型定义);在插入和删除的过程中,它们自己进行必要的内存管理;它们报告自己有多少对象,最多能容纳多少对象(size、max_size);当然,当它们自身被析构时,自动析构所包含的每个对象。
但当容器包含的是通过new的方式而分配的指针时,如果不做必要的善后清理工作,可能会导致资源泄漏。
STL容器很智能,但没有智能到知道是否该删除自己所包含的指针的程度。当使用指针的容器,而其中的指针应该被删除时,为了避免资源泄漏,应该用引用计数形式的智能指针对象(比如Boost的shared_ptr)代替指针,或者当容器被析构时手工删除(delete)其中的每个指针。
第八条 切勿创建包含auto_ptr的容器对象(20190717)
auto_ptr的容器(简称COAP)是被禁止的。书中提到使用它们的代码不会被编译通过,但笔者用DevC++试验是可以通过的,这个可能与编译器有关。但千万不要用包含auto_ptr的容器,这主要与auto_ptr的特性有关。
在复制一个auto_ptr时,它所指向的对象的所有权被移交到拷入的auto_ptr上,而它自身被置为NULL。
包含智能指针的容器是存在的,但auto_ptr不是这样的智能指针。
第九条 慎重选择删除元素的方法(20190718)
要删除容器中有特定值的所有对象:如果容器是vector、string或deque,则使用erase-remove习惯用法;如果容器是list,则使用list::remove;如果容器是一个标准关联容器,则使用它的erase成员函数。
要删除容器中满足特定判别式(条件)的所有对象:如果容器是vector、string或deque,则使用erase-remove_if习惯用法;如果容器是list,则使用list::remove_if;如果容器是一个标准关联容器,则使用remove_copy_if和swap,或者写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对它进行后缀递增。
要在循环内部做某些(除了删除对象之外的)操作:如果容器是一个标准序列容器,则写一个循环来遍历容器中的元素,记住每次调用erase时,要用它的返回值更新迭代器;如果容器是一个标准关联容器,则写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对迭代器做后缀递增。(注意这里是建立在标准关联容器erase方法返回值为void为基础上的)
第十条 了解分配子(allocator)的约定和限制(20190721)
如果希望编写自定义的分配子,需要记住以下内容:
- 分配子是一个模板,模板参数T代表为它分配内存的对象的类型。
- 提供类型定义pointer和reference,但是始终让pointer为T*,reference为T&。
- 千万别让分配子拥有随对象而不同的状态(per-object state)。通常,分配子不应该有非静态的数据成员。
- 传给分配子的allocate成员函数的是那些要求内存的对象的个数,而不是所需的字节数。同时要记住,那些函数返回T*指针(通过pointer类型定义),即使尚未有T对象被构造出来。
- 一定要提供嵌套的rebind模板,因为标准容器依赖该模板。
第十一条 理解自定义分配子的合理用法
略
第十二条 切勿对STL容器的线程安全性有不切实际的依赖
略
第十三条 vector和string优先于动态分配的数组(20190722)
当决定用new来动态分配内存时,这意味着将承担以下责任:
- 必须确保以后用delete来删除所分配的内存,否则会导致资源泄漏
- 必须确保使用了正确的delete形式,如果分配了单个对象,则必须使用"delete";如果分配了数组,则需要使用"delete[]"。如果使用了不正确的delete形式,那么结果将是不确定的。在有些平台上,程序会在运行时崩溃;在其他平台上,它会妨碍进一步运行,有时会泄漏资源和破坏内存。
- 必须确保只delete了一次。如果一次分配被多次delete,结果同样是不确定的。
如果正在动态地分配数组,那么可能要做更多的工作。为了减轻自己的负担,使用vector或string。
第十四条 使用reserve来避免不必要的重新分配(20190722)
关于STL容器,最了不起的一点是它们会自动增长以便容纳下要放入其中的数据,只要没有超过它们的最大限制即可(调用max_size()函数查看,我调用的的结果是4611686018427387897)。
对于vector和string,增长过程是这样实现的:每当需要更多空间时,就调用与realloc类似的操作:
- 分配一块大小为当前容量的某个倍数的新内存。在大多数实现中,vector和string的容量每次以2的倍数增长,即每当容器需要扩张时,它们的容量就加倍。
- 把容器的所有元素从旧的内存复制到新的内存中。
- 析构掉旧内存中的对象。
- 释放旧内存
reverse成员函数能使重新分配的次数减少到最低限度,从而避免了重新分配和指针/迭代器/引用失效带来的开销。
在标准容器中,只有vector和string提供了以下这4个函数:
- size(),return size。返回容器中元素的个数
- capacity(),return size of allocated storage capacity。返回容器利用已经分配的内存可以容纳元素的个数。若想知道一个vector还有多少未被使用的内存,就得从capacity()中减去size()。如果capacity()和size()返回同样的值,则容器中不再有剩余空间了,因此下一个插入操作将导致上述重新分配过程。
- resize(),Change size。强迫容器改变到包含n个元素的状态。调用resize(n)后,size()将返回n。如果n比当前的size()要小,则容器尾部的元素将会被析构。如果n比当前的size()要大,则通过默认构造函数创建的新元素被添加到容器的末尾。如果n比当前的容量要大,那么在添加元素之前,将先重新分配内存。
- reserve(),Request a change in capacity。强迫容器的容量至少为n,前提是n不小于当前的大小。这通常会导致重新分配内存,因为容量需要增加。
当一个元素需要被插入而容器的容量不够时,就会发生重新分配过程(包括原始内存的分配和释放,对象的复制和析构,迭代器、指针和引用的失效)。因此,避免重新分配的关键在于,尽早地使用reserve,把容器的容量设为足够大的值,最好是在容器刚被构造出来之后就使用reserve。
第十五条 注意string实现的多样性(20190723)
- string的值可能会被引用计数,也可能不会。很多实现在默认情况下会使用引用计数,但它们通常提供了关闭默认选择的方法,往往是通过预处理宏来做到这一点。
- string对象大小的范围可以是一个char*指针的大小的1~7倍。
- 创建一个新的字符串值可能需要零次、一次或两次动态分配内存。
- string对象可能共享,也可能不共享其大小和容量信息。
- string可能支持,也可能不支持针对单个对象的分配子。
- 不同的实现对字符内存的最小分配单位有不同的策略。
第十六条 了解如何把vector和string数据传给旧的API(20190723)
对于vector v;表达式v[0]给出了一个引用,它是该矢量中的第一个元素,所以&v[0]是指向第一个元素的指针。C++标准要求vector中的元素存储在连续的内存中,就像数组一样。所以,如果希望把v传给一个如下所示的C API:
void doSomething(const int* pInts, size_t numInts);
那么可以这样做:
if(!v.empty()) doSomething(&v[0], v.size())
//避免v是空的,避免&v[0]产生一个指针,而该指针指向的东西并不存在
不要用v.begin()来代替&v[0],虽然begin返回vector的迭代器,而对于vector来说,迭代器实际上就是指针,通常这是正确的,但是事实并不总是这样。当需要一个指向vector中的数据的指针时,永远不应该使用begin。如果为了某种原因需要使用v.begin(),那么请使用&*v.begin(),因为这和&v[0]产生同样的指针。
上述得到容器中数据指针的方式对于vector是适用的,但对于string却是不可靠的。因为:(1)string中的数据不一定存储在连续的内存中;(2)string的内部表示不一定是以空字符结尾的。这也说明了为什么在string中存在成员函数c_str。c_str函数返回一个指向字符串的值的指针,而且该指针可用于C。因此,可以把一个字符串s传给下面的函数:
void doSomething(const char* pString);
如下所示:
doSomething(s.cstr());
即使字符串的长度是零,这么做也是可以的。在这种情况下,c_str会返回一个指向空字符的指针。对字符串内部有空字符的情况也是可以的。但是,在这种情况下,doSomething会把内部的第一个空字符当做结尾的空字符。string对象中包含空字符没关系,但是对基于char*的C API则不行。
再看一下doSomething的声明:
void doSomething(const int* pInts, size_t numInts);
void doSomething(const char* pString);
在这两种情况下,要传入的指针都是指向const的指针。vector或string的数据被传递给一个要读取,而不是改写这些数据的API。到现在为止,这是最安全的方式。对于string,这也是唯一所能做的,因为c_str所产生的指针并不一定指向字符串的内部表示;它返回的指针可能是指向字符串数据的一个不可修改的副本,该副本已经被做了适当的格式化,以满足C API的要求。
对于vector,多了一点灵活性。如果传递的C API改变了v中元素值的话,通常是没有问题的(有些矢量对它们的数据有额外的限制,必须保证这些额外的限制还能被满足),但被调用的例程不能试图改变矢量中元素的个数。
如果想用来自C API中的元素初始化一个vector,那么可以利用vector和数组的内存布局兼容性,向API传入该矢量中元素的存储区域:
//C API :该函数以一个指向最多有arraySize个doube类型数据的数组的指针为参数,并
//向该数组中写入数据。它返回已被写入的double数据的个数,这个数不会超过arraySize。
size_t fillArray(double* pArray, size_t arraySize);
vector<double> vd(maxNumDoubles); //创建大小为maxNumDoubles的vector
vd.resize(fillArray(&vd[0],vd.size())); //使用fillArray向vd中写入数据,然后把vd的大小改为fillArray所写入的元素的个数
这一技术只对vector有效,因为只有vector才保证和数组有同样的内存布局。不过,若想用来自C API中的数据初始化一个string,也很容易能做到。只要让API把数据放到一个vector< char>中,然后把数据从该矢量复制到相应字符串中:
//C API :该函数以一个指向最多有arraySize个char类型数据的数组的指针为参数,并
//向该数组中写入数据。它返回已被写入的char数据的个数,这个数不会超过arraySize。
size_t fillString(double* pArray, size_t arraySize);
vector<char> vc(maxNumChars); //创建大小为maxNumChars的vector
size_t charsWritten = fillString(&vc[0], vc.size()); //使用fillString向vc中写入数据
string s(vc.begin(), vc.begin()+charsWritten); //通过区间构造函数,把数据从vc复制到s中
实际上,先让C API把数据写入到一个vector中,然后把数据复制到期望最终写入的STL容器中,这一思想总是可行的:
//C API :同上
size_t fillArray(double* pArray, size_t arraySize);
vector<double> vd(maxNumDoubles);
vd.resize(fillArray(&vd[0],vd.size()));
deque<double> d(vd.begin(), vd.end()); //把数据复制到deque中
list<double> l(vd.begin(), vd.end()); //把数据复制到list中
set<double> s(vd.begin(), vd.end()); //把数据复制到set中
而且这也意味着,除了vector和string以外的其他STL容器也能把它们的数据传递给C API。只需把每个容器的元素复制到一个vector中,然后传给该API。
第十七条 使用"swap"技巧除去多余的容量(20190725)
按下面的做法,可以从contestants矢量中除去多余的容量:
vector< Contestant>(contestants).swap(contestants);
表达式vector< Contestant>(contestants)创建一个临时的矢量,它是contestants的副本:这是由vector的复制构造函数来完成的。然而vector的复制构造函数只为所复制的元素分配所需要的内存,所以这个临时矢量没有多余的容量。然后把临时矢量中的数据和contestants中的数据做swap操作,在这之后,contestants具有了被去除之后的容量即原先临时变量的容量,而临时变量的容量变成了原先contestants臃肿的容量。到这时(在语句结尾),临时矢量被析构,从而释放了先前为contestants所占据的内存。
swap技巧的一种变化形式可以用来清除一个容器,并使其容量变为该实现下的最小值。只要与一个用默认构造函数创建的vector或string做交换(swap)就可以了。
vector<Contestant> v;
string s;
...
vector<Contestant>().swap(v); //清除v并把它的容量变为最小
string().swap(s);
在做swap的时候,不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换(string除外)。在swap之后,原先指向某容器中元素的迭代器、指针和引用依旧有效,并指向同样的元素(这些元素已经在另一个容器中了)。
第十八条 避免使用vector< bool>(20190725)
vector< bool>是一个假的容器,它并不真的储存bool,相反,为了节省空间,它储存的是bool的紧凑表示。在一个典型的实现中,储存在"vector"中的每个"bool"仅占一个二进制位,一个8位的字节可容纳8个"bool"。在内部,vector< bool>使用了与位域(bitfield)一样的思想,来表示它所存储的那些bool;实际上它只是假装存储了这些bool。
位域与bool相似,它只能表示两个可能的值,i在bool和看似bool的位域之间有一个很重要的区别:可以创建一个指向bool的指针,而指向单个位的指针则是不允许的。
vector< bool>不完全满足STL容器的要求;最好不要用它;可以用deque< bool>和bitset来代替它,这两个数据结构几乎能做vector< bool>所能做的一切事情。
第十九条 理解相等(equality)和等价(equivalence)的区别(20190726)
在实际操作中,相等的概念是基于operator= =的。如果表达式“x= =y”返回真,则x和y的值相等,否则就不相等。但是,x和y有相等的值并不一定意味着它们的所有数据成员都有相等的值,这取决于==里的的内容。
等价关系是以“在已排序的区间中对象值的相对顺序”为基础的。如果从每个标准关联容器(即set、multiset、map、multimap,排列顺序也是这些容器的一部分)的排列顺序来考虑等价关系,那么将是非常有意义的。
如果两个对象x和y,如果按照关联容器c的排列顺序,每个都不在另一个的前面,那么称这两个对象按照c的排列顺序有等价的值。
考虑set< Widget> s。如果两个Widget w1和w2,在s的排列顺序中哪个也不在另一个的前面,那么w1和w2对于s而言有等价的值。set< Widget>的默认比较函数是less< Widget>,而在默认情况下less< Widget>只是简单地调用了针对Widget的operator < ,所以,如果下面的表达式结果为真,则w1和w2对于operator <有等价的值:
!(w1<w2)&&!(w2<w1) //w1<w2不为真而且w2<w1不为真
这里的含义是:如果两个值中的任何一个(按照一定的排序准则)都不在另一个前面, 这两个值(按照这一准则)就是等价的。
在一般情形下,一个关联容器的比较函数并不是operator <,甚至也不是less,它是用户定义的判别式(predicate)。每个标准关联容器都通过key_comp成员函数是排序判别式可被外部使用。所以,如果下面的表达式为true,则按照关联容器c的排序准则,两个对象x和y有等价的值:
!c.key_comp()(x,y)&&!c.keycomp()(y,x)//在c的排列顺序中,x在y之前不为真,y在x之前也不为真
c.key_comp()返回一个函数(或者一个函数对象),!c.key_comp()(x,y)调用key_comp()返回的函数(或函数对象),并以x和y作为传入参数。然后把结果取反。只有当x按照c的排列顺序在y之前时,c.key_comp(x,y)才返回真。
标准关联容器总是保持排列顺序的,所以每个容器必须有一个比较函数(默认为less)来决定保持怎样的顺序。等价的定义正是通过该比较函数而确定的,因此,标准关联容器的使用者要为所使用的每个容器指定一个比较函数(用来决定如何排序)。
第二十条 为包含指针的关联容器指定比较类型(20190726)
以set<string*> ssp为例,它是set<string*, less<string*> >ssp的缩写,最准确的说是set<string*, less<string*>, allocator<string*> >ssp的缩写,但是分配子与此条款中讨论的问题无关,故不考虑它。
如果想让string指针在集合中按照字符串的值排序,那么不能使用默认的比较函数子类(functor class)less<string>。必须自己编写比较函数子类,该类的对象以string*指针为参数,并按照它们所指向的string的值进行排序,就像这样:
struct StringPtrLess:public binary_function<const string*, const string*, bool>
{
bool operator()(const string *ps1, const string *ps2) const
{
return *ps1<*ps2;
}
};
然后可以用StringPtrLess作为ssp的比较类型:
typedef set<string*,StringPtrLess> StringPtrSet;
StringPtrSet ssp;
每当要创建包含指针的关联容器时,一定要记住,容器会按照指针的值进行排序。绝大多数情况下这不会是所希望的,所以几乎肯定要创建自己的函数子类作为该容器的比较类型。
set模板的三个参数每个都是一个类型。将一个返回值为bool的函数作为set的参数是不能通过编译的。
本条款是关于包含指针的关联容器的,也同样适用于其他一些容器,这些容器中包含的对象与指针的行为相似,比如智能指针和迭代器。
第二十一条 总是让比较函数在等值情况下返回false(20190728)
除非你的比较函数对相等的值总是返回false,否则你会破坏所有的标准关联容器,不管它们是否允许存储重复的值。
比较函数的返回值表明的是按照该函数定义的排列顺序,一个值是否在另一个之前。相等的值从来不会有前后顺序关系,所以,对于相等的值,比较函数应当始终返回false。
第二十二条 切勿直接修改set或multiset中的键(20190728)
如果改变了set或multiset中的元素,请记住,一定不要改变键部分(key part),——元素的这部分信息会影响容器的排序性。如果改变了这部分内容,那么可能会破坏该容器,再使用该容器将导致不确定的结果。另一方面,这项限制只适用于被包含对象的键部分。对于被包含元素的其他部分,则完全是开放的。
如果想以一种总是可行而且安全的方式来修改set、multiset、map和multimap中的元素,则可以分成5个简单步骤来进行:
- 找到想修改的容器的元素。
- 为将要被修改的元素做一份副本。
- 修改该副本,使其具有所期望的值
- 把该元素从容器中删除,通常是通过调用erase来进行的。
- 把新的值插入到容器中。
关键要记住,对set和multiset,如果直接对容器中的元素做了修改,那么要保证该容器是排好序的。
第二十三条 考虑用排序的vector替代关联容器(20190729)
很多应用程序使用其数据结构的方式并不这么混乱。它们使用其数据结构的过程可以明显地分为3个阶段,总结如下:
- 设置阶段。创建一个新的数据结构,并插入大量元素。在这个阶段,几乎所有的操作都是插入和删除操作。很少或几乎没有查找操作。
- 查找阶段。查询该数据结构以找到特定的信息。在这个阶段,几乎所有的操作都是查找操作,很少或几乎没有插入和删除操作。
- 重组阶段。改变该数组结构的内容,或许是删除所有的当前数据,再插入新的数据。在行为上,这个阶段与第1阶段类似。当这个阶段结束以后,应用程序又回到第2阶段。
对以这种方式使用数据结构的应用程序来说,vector可能比关联容器提供了更好的性能(时间、空间)。
在排序的vector中存储数据可能比在标准关联容器中存储同样的数据要耗费更少的内存,而考虑到页面错误的因素,通过二分搜索法来查找一个排序的vector可能比查找一个标准关联容器要更快一些。
查找操作几乎从不跟插入和删除操作混在一起时,再考虑使用排序的vector而不是关联容器才是合理的。
第二十四条 当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择(20190731)
对于map<K,V> m,表达式m[k]=v 检查键k是否在map中了。如果没有,它就被加入,并以v作为相应的值。如果k已经在映射表中了,则与之关联的值被更新为v。
当效率至关重要时,如果要更新一个已有的映射表元素,则应该优先选择oprator[];但如果要添加一个新的元素,那么最好还是选择insert。
第二十五条 熟悉非标准的散列容器(20190731)
C++11已经引入了标准的散列容器(?)
http://www.cplusplus.com/reference/unordered_set/unordered_set/
第二十六条 iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator(20190802)
书中提到,vecot中的insert函数仅接受iterator类型的参数,但很遗憾,C++11已经和C++98不一样了,我比较怀疑我继续读下去是否有益(已经有若干条存在版本问题了),我应该找最新版的。
第二十七条 使用distance和advance将容器的const_iterator转换成iterator(20190802)
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
ConstIter ci;
...
Iter i(ci);//编译错误,从const_iterator到iterator没有隐式转换途径
Iter i(const_cast<Iter>(ci));//仍然是编译错误,不能将const_iterator强制转换为iterator
如果有一个const_iterator并且可以访问它所在的容器,那么有一条安全的、可移植的途径能得到对应的iterator,而且不用设计类型系统的强制转换。
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
IntDeque d;
ConstIter ci;
...
Iter i(d.begin());
advance(i,distance<ConstIter>(i,ci));
为了得到一个与const_iterator指向同一位置的iterator,首先创建一个新的iterator,将它指向容器的起始位置,然后取得cosnt_iterator距离容器起始位置的偏移量。
第二十八条 正确理解由reverse_iterator的base()成员函数所产生的iterator的用法(20190804)
如果要在一个reverse_iterator ri指定的位置上插入新元素,则只需在ri.base()位置处插入元素即可。对于插入操作而言,ri和ri.base()是等价的,ri.base()是真正与ri对应的iterator。
如果要在一个reverse_iterator ri指定的位置上删除一个元素,则需要在ri.base()前面的位置上执行删除操作。对于删除操作而言,ri和ri.base()是不等价的,ri.base()不是与ri对应的iterator。(此时对应的是–ri.base(),若不能通过编译,改为(++ri).base() )即可)
将一个reverse_iterator转换成iterator的时候,很重要的一点是,必须清楚将要对该iterator执行什么样的操作。
第二十九条 对于逐个字符的输入请考虑使用istreambuf_iterator
略
第三十条 确保目标区间足够大(20190805)
不论何时,如果所使用的算法需要指定一个目标区间,那么必须确保目标区间足够大,或者确保它会随着算法的运行而增大。要在算法执行过程中增大目标区间,请使用插入型迭代器,比如ostream_iterator,或者由back_inserter、front_inserter和inserter返回的迭代器。
第三十一条 了解各种与排序有关的选择(20190805)
- 如果需要对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算法。
除此之外,也可以通过标准的关联容器来保证容器中的元素时钟保持特定的顺序。
第三十二条 如果确实需要删除元素,则需要在remove这一类算法之后调用erase(20190806)
用remove从容器中删除元素,而容器中的元素数目并不会因此而减少。remove不是真正意义上的删除,因为它做不到。(此处的remove指的是< algorithm >里的remove,下同,只有list有remove成员函数list::remove)
remove移动了区间中的元素,其结果是,“不用被删除”的元素移动到了区间的前部(保持原来的相对顺序)。它返回的是一个迭代器指向最后一个“不用被删除”的元素之后的元素。这个返回值相当于该区间“新的逻辑结尾”。一般情况下在新的逻辑结尾后面的元素仍然保留其旧的值。
通常来说,当调用了remove以后,从区间中被删除的那些元素可能在也可能不在区间中。
如果想删除那些元素,只需调用区间形式的erase,并将这两个迭代器传递给它。因为remove返回的迭代器正是新的逻辑结尾。如下所示:
vector<int> v;
...
v.erase(remove(v.begin(),v.end(),99),v.end());//真正删除所有值等于99的元素
把remove返回的迭代器作为区间形式的erase的第一个实参是很常见的,只是个习惯用法。事实上,remove和erase的配合是如此紧密,以致它们被合并起来融入到了list的remove成员函数中。这是STL中唯一一个名为remove并且确实删除了容器中元素的函数。
remove和remove_if相似,unique也和remove行为相似,真正删除元素也需要和erase结合。
第三十三条 对包含指针的容器使用remove这一类算法时要特别小心(20190806)
当容器中存放的是指向动态分配的指针的时候,应该避免使用remove和类似的算法(remove_if和unique)。很多情况下,partition算法是个不错的选择。
处理存放动态分配的指针的容器时,可以这样:或者通过引用计数的智能指针,或者在调用remove类算法之前先手工删除指针并将它们置为空等。总之,对包含指针的容器使用remove类算法时需要特别警惕。不留意的话会造成资源泄漏。
第三十四条 了解哪些算法要求使用排序的区间作为参数(20190806)
并非所有的算法都可以应用于任何区间。举例来说,remove算法要求单向迭代器并且要求可以通过这些迭代器向容器中的对象赋值。所以,它不能用于由输入迭代器指定的区间,也不适用于map或multimap,同样不适用于某些set和multiset的实现。
STL中那些要求排序区间的STL算法:
- 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要求排序的区间是因为用二分法查找数据,承诺了对数时间的查找效率,但前提是按顺序拍好的数据。实际上,这些算法并不一定保证对数时间的查找效率,只有当它们接受了随机访问迭代器的时候,它们才保证有这样的效率。如果所提供的迭代器不具备随机访问的能力(比如双向迭代器),那么,尽管比较次数仍然是区间元素个数的对数,但它们的执行过程却需要线性时间,这是因为缺少了执行“迭代器算术”的能力,所以在查找过程中它们需要线性时间以便从区间的一处移动到另一处。
set_union、set_intersection、set_difference和set_symmetric_difference这4个算法提供了线性时间效率的集合操作。
merge和inplace_merge实际上实现了合并和排序的联合操作:它们读入了两个排序的区间,然后合并成一个新的排序区间。线性时间。
includes用来判断一个区间中的所有对象是否都在另一个区间中。
unique和unique_if与上述讨论过的算法有所不同,它们即使对于未排序的区间也有很好的行为。
所有要求排序区间的算法(本条款提到的除了unique和unique_copy以外的算法)均使用等价性来判断两个对象是否“相同”,这与标准的关联容器一致。与此相反的是,unique和unique_copy在默认情况下使用“相等”来判断两个对象是否“相同”。
第三十五条 通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较(20190807)
mismatch算法是比较两个序列,找出首个不匹配元素的位置。
lexicographical_compare函数是按照字典序测试[frist1,last1)是否小于[first2,last2).
由于书中把焦点集中在可移植型上,略。
http://www.cplusplus.com/reference/algorithm/mismatch/?kw=mismatch
http://www.cplusplus.com/reference/algorithm/lexicographical_compare/
第三十六条 理解copy_if算法的正确实现(20190807)
C++11中又有了copy_if:
// copy_if example
#include <iostream> // std::cout
#include <algorithm> // std::copy_if, std::distance
#include <vector> // std::vector
int main () {
std::vector<int> foo = {25,15,5,-5,-15};
std::vector<int> bar (foo.size());
// copy only positive numbers:
auto it = std::copy_if (foo.begin(), foo.end(), bar.begin(), [](int i){return !(i<0);} );
bar.resize(std::distance(bar.begin(),it)); // shrink container to new size
std::cout << "bar contains:";
for (int& x: bar) std::cout << ' ' << x;
std::cout << '\n';
return 0;
}
Output:
bar contains: 25 15 5
第三十七条 使用accumulate或者for_each进行区间统计(20190808)
http://www.cplusplus.com/reference/numeric/accumulate/?kw=accumulate
http://www.cplusplus.com/reference/algorithm/for_each/?kw=for_each
第三十八条 遵循按值传递的原则来设计函数子类
略
第三十九条 确保判别式是“纯函数”
略
第四十条 若一个类是函数子,则应使它可配接
略
第四十一条 理解ptr_fun、men_fun和mem_fun_ref的来由
略
第四十二条 确保less< T >与operator<具有相同的语义
略
第四十三条 算法调用优先于手写的循环
略
第四十四条 容器的成员函数优先于同名的算法
略
第四十五条 正确区分count、find、binary_search、lower_bound、upper_bound和equal_range
略
第四十六条 考虑使用函数对象而不是函数最为STL算法的参数
略
第四十七条 避免产生“直写型”(write-only)的代码
略
第四十八条 总是包含(#include)正确的头文件
略
第四十九条 学会分析与STL相关的编译器诊断信息
略
第五十条 熟悉与STL相关的Web站点(20190808)
SGI STL站点:
https://community.hpe.com/t5/Servers-The-Right-Compute/SGI-com-Tech-Archive-Resources-now-retired/ba-p/6992583#.XUwm6x0zbIU
(貌似没了,https://www.martinbroadhurst.com/stl/这里可以访问到)
STLport站点:
http://www.stlport.org/
Boost站点:
https://www.boost.org/
参考资料:
0、Effective STL 中文版,Scott Meyers著,潘爱民 陈铭 邹开红译,2013年1月第一版
1、标准非STL容器:bitset
2、STL Rope-when and where to use
3、http://www.cplusplus.com/
4、说一说vector < bool >
5、auto_ptr作为vector的元素会出现什么情况
6、令人疑惑的std::remove算法
7、vector内存增长方式