以下为这两天看C++ STANDARD LIBRARY的总结,都是以前不够清楚的地方:
c++标准库主要包括STL,iostream,string等。
STL包括:1.容器containers,2.迭代器iterators,3.算法algorithms,4.仿函数functors,5.适配器adapters
容器包括:
1.序列容器,有vector,deque,list。其中vector为动态数组,暂且猜测是用数组实现,每次内存不够就double之。deque猜测与vector类似,只是不光后面有空余位置,前面也有,任何一部分不够用的时候都需要重新分配内存。list就是链表,基于效率考虑不提供随机访问接口,也就不能用[]来取得容器内元素。
2.关联容器,有set,map,multiset,multimap。这类容器在插入值的时候,值的位置与插入顺序无关,而只与值本身有关,因为它们是时刻保持有序的,用二叉树实现(红黑树),后两者为前两者的允许有重复值出现的版本。
3.容器适配器,这其实属于adapters,不过也可以放在这里,所谓adapters,就是一层封装,比如容器适配器是在基础容器外封装一层得到一些提供特殊功能的容器,而迭代器适配器则是通过封装(待验证)得到一些有特殊功能的迭代器,这两种适配器分别满足容器接口和迭代器接口,因此都可以与algorithm配合使用。容器适配器有stack,queue,priority_queue。
容器会调用拷贝构造函数来向容器添加元素,如push_back()就会调用拷贝构造函数,因此容器内存放的是元素的副本,而不是引用,也就是说STL容器是值语义的而不是引用语义的。容器通过缺省构造函数来初始化非空容器和调用resize(),如vector v(5),会调用5次A类的缺省构造函数,生成五个对象放入v内。容器还需要元素的operator=来对元素进行更新。在将元素从容器中移除时还会调用析构函数。通过元素的operator==进行查找,通过operator<(实际上是仿函数less<>)进行排序。因此如果希望自定义类能够放在容器里,应该实现以上接口。
是动态数组,因此在结尾处插入和删除都有不错效率,在前面或中间位置插入和删除则效率较低,会引起后面元素的全部改写,也会引起后面的迭代器、指针全部失效。数组的后面预留了一定的位置,不断地在结尾处插入新元素会导致内存不足,从而导致内存的重新分配,具体分配多少(是否double)是跟具体实现有关的,但一般编译器的实现都是在一开始就分配一整块内存(2K),因此如果你有很多vector,但其中元素都很少,那内存浪费还是很可观的。为了避免内存频繁被重新分配可以调用reserve()预先保留足够量的内存,size()返回当前元素个数,capacity()返回在不需要重新分配内存的情况下当前vector的容量,max_size()返回编译器支持的最多元素数。不断地在结尾处删除元素并不会使所占内存缩减,有一个缩减内存的方法是用拷贝构造函数得到一个当前容器的临时副本(它的容量应该是合适的最小值),然后swap(v,tmp),再删除tmp。
C++标准保证vector的内存是连续的,即保证对于vector
并不是简单的双向动态数组,而是多块内存,其中第一块可以向前生长,最后一块可以向后生长。因为不是连续内存,因此普通纸真不能胜任迭代器的工作,需要智能指针,因为加了这一层,所以deque的元素访问和迭代器动作会比vector稍微慢一点点。任何在开头和结尾添加和删除元素的动作都可以高效的完成,且不会使引用和指针失效,但是迭代器会失效。除了开头和结尾,在其它任何位置进行添加和删除元素都会使所有指针、引用、迭代器失效。deque不提供reserve()和capacity()(猜测是因为其内存模型比较复杂)。另外,由于其可以占用多块内存,因此在单块内存大小有限制的系统中,deque的max_size()要比vector的大。
是一个双向链表。由于效率较低,所以list索性不提供随机存取功能,即不可以通过下标和对迭代器的算术运算来随机访问list内的元素,能够直接访问到的元素只有第一个和最后一个(分别通过.front()和.back(),因为是双向链表嘛)。在任何位置进行插入和删除都非常高效,且不会影响任何指针、引用和迭代器的有效性。因为结构的特殊性,所以STL算法中的一些算法应用在list上会表现出很低的效率(因为它们根本不知道自己操纵的容器是list),因此list提供了很多算法的成员函数版本,使用它们进行操作可以获得很好的效率。这样的成员函数包括:sort,remove,remove_if。
由于list只提供bidirectional iterator而不提供random access iterator,因此那些需要random access iterator的STL算法均不可以使用,如sort,但是list提供了自己的成员函数sort()用以代替(效率如何?TBC)。
由于结构的特殊性,使得list执行某些有意义的操作会取得比其它容器好得多的效率,因为只需要更改几个指针,因此list提供了这些成员函数,他们是:
void list::unique(),如果有多个相同元素相邻,则删除重复元素,只保留一个。
void list::unique(BinaryPredicate op),与上面的类似,只不过通过一个二元判别式op(x,y)来判断两个函数是否相同,元素是与未被移除的前一元素比较,而不是紧挨着的前一元素,后者可能在比较到当前元素时就已经被移除了。这两个函数都会导致被移除函数的析构函数被调用。
void list::splice(iterator pos,list& source),将source所指代list的所有元素都安插到当前list的pos位置上。
void list::splice(iterator pos,list& source,iterator sourcePos),将source中某个元素(由sourcePos指定)移动到当前list的pos处。
void list::splice(iterator pos,list&source,iterator sourceBeg,iterator sourceEnd),如参数所述。this和source可以是同一个list。
void list::sort()
void list::sort(CompFunc op),排序,是稳定排序。
void list::merge(list& source)
void list::merge(list& source,CompFunc op),二路归并,两个list内的元素顺序在归并得到的新list中保持不变,两者元素间顺序取决于operator<或op,就像归并排序那样。
void list::reverse(),颠倒顺序。
关联容器都是用平衡二叉树作为其数据结构(一般为红黑树),利用某种比较原则(operator<或者某op())将元素插入合适的位置,从而维持整个容器有序,但因为如此使得关联容器不能对元素进行直接更改,而且提供的迭代器都是const_iterator,因为否则就会破坏有序性,妥协的做法是先删除再重新插入。
{猜测:在关联容器上迭代只能从头到尾,而且是顺序迭代,因为是二叉排序树而且只能从头到尾(中间可跳出)地迭代,因此可以使用二叉树的中序遍历}
与list在删插元素方面有高效率类似,由于结构的特殊性(平衡二叉树),关联容器在搜寻元素方面有高效率,因此提供了STL搜寻算法的成员函数版本。这些成员函数提供对数复杂度,而STL算法则提供线性复杂度(两者都是显然的)。这些成员函数是count(elem),find(elem),lower_bound(elem),upper_bound(elem),equal_range(elem)。
STL的remove()算法实际上是以后面的元素值覆盖前面的,因此不能对关联容器调用remove(),只能调用其成员erase()。
关联容器模板参数除了元素类型外,还有隐含的比较准则(一个仿函数,默认为less<>),因此它也参与决定关联容器的类型,两个只有排序准则不同的关联容器被认为是不同类型的,尽管它们之间可以相互赋值(赋值动作包括排序准则的赋值)。
说到map和multimap,数据结构和set/multiset相同,因此性质类似,按key搜寻很快,按value搜寻很慢(而且不提供接口,只能通过通用的find_if()或者自定义循环),不能更改key,可以更改value。如果想更改key的话,可以先插入一个pair
关联容器的删除问题:对于set来说,可以调用erase(elem)删除所有值为elem的元素,并返回删除个数,或者调用erase(pos)删除指定迭代器位置的元素。对于map,如果要按照key或者迭代器来删除,那么和set相同,但如果要按照value删除,则有一些问题。如果你要删除所有指定value的项,你这样写:
for(pos = m.begin();pos!=m.end();++pos){if(pos->second == value)m.erase(pos);}
则会引起RUNTIME ERROR,因为当被调用erase()后,pos会成为一个失效的迭代器(它所指向的节点已经被删除,二叉树可能也已经调整,当然不管二叉树有没有调整,该迭代器都已经无效了,即不能通过它自增来找到原本是它所指结点的下一个节点了),对它进行operator++会导致未定义行为。正确的写法是:
for(pos = m.begin();pos != m.end()){if(pos->second==value)m.erase(pos++);}
这个之所以有效是因为会先保留一个pos的临时对象,再将pos自增1从而指向应该指向的下一个元素(避免失效),然后将临时对象传递给erase()进行正确的删除。虽然接下来可能二叉树会进行调整,但是pos已经指向了正确的节点,在调整之后也不会失效。
有三种办法实现自己的容器(所谓容器,主要指的是可以让STL算法通过迭代器接口作用在其上):1.侵入式方法,完全按照STL容器的方式,提供所需的全部接口,这相当于侵入了自定义类的定义,string就是这样的例子,string虽然不是模板,但是它提供了容器所需的所有接口:.begin(),::iterator等等。2.数组。3.外包一层。
迭代器包括:
1.bidirectional iterator双向迭代器,这种迭代器只提供operator++,operator--,不提供算术操作,也不提供operator<,因此只能使用it != l.end(),而不能使用it < l.end(),这也是为什么有的书上提倡使用前面那种判断方法的原因。list,set,multiset,map,multimap提供的迭代器均属此类。计算两个bidirectional iterator之间距离的时候需要借用辅助函数distance(),而对于下面的random access iterator只需要调用operator-就可以了。
2.random access iterator随机存取迭代器,这种迭代器下层可能是用指针实现的(待验证),因此提供算术操作(it+=3),提供operator<。
3.迭代器适配器,同样是adapters。包括
a)insert iterator(back_inserter-要求容器提供push_back()(vector,list,deque均提供),front_inserter-要求容器提供push_front()(list,deque均提供),inserter-要求容器提供insert()(vector,deque,list,set,map,multiset,multimap均提供,因此是唯一可用于关联容器的insert iterator,然而对于关联容器,由于其本身的有序性,因此这个inserter并不是指定插入位置,而是指定开始搜索插入位置的位置));
b)stream iterator,包括istream_iterator,ostream_iterator,可以在流上迭代。
c)reverse iterator,就是revers_iterator,每种容器都提供rbegin(),rend()函数与之配合使用,从而可以反向遍历容器。
还可以定义const_iterator,保证其指向的对象不可更改(而不是该迭代器不可更改,是可以更改自身的值而指向其它对象的),关联容器的迭代器全部默认为const_iterator,这是为了维护关联容器的有序性(sorted)。
算法包括:
TBC
仿函数是:
仿函数就是行为像函数的object,也就是可以通过小括号传参数的方式调用某种功能的object,说穿了就是实现了operator()的类的对象,一般用ClassName(arguments…)的方式生成一个临时对象,在通过这个临时对象调用其operator()。由于是类,因此可以用成员变量记录状态,因此仿函数的功能要强于普通函数。这里也存在adapter的概念,用于更改参数个数(TBC)。
TBC