《C++标准程序库》第五章摘录与笔记

来源:互联网 发布:网络机柜 尺寸 编辑:程序博客网 时间:2024/06/08 19:38

《C++标准程序库》第五章摘录与笔记

STL提供的六大组件:

1、容器(containers):各种数据结构,用来存放数据,用来管理某类对象的集合。从实现的角度看,STL容器是一种class template。
2、算法(algorithms):各种常用算法,用来处理群集内的元素。从实现的较短来看,STL算法是一种function template。
3、迭代器(iterators):扮演容器和算法之间的胶合剂,是所谓的“泛型指针”,用来在一个对象群集的元素上进行遍历操作。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将operator*,operator->,operator++,operator--等指针相关操作予以重载的class template。所有的STL容器都附带有自己专属的迭代器,只有容器设计者才知道如何遍历自己的元素。原生指针(native)也是一种迭代器。
4、仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般函数指针可以视为侠义的仿函数。
5、配接器(adapters):一种用来修饰容器(containers)或仿函数(functors)或迭代器(iterators)接口的东西。改变functor接口者,称为function adapter;改变container接口者,称为container adapter;改变iterator接口者,称为iterator adapter。
6、配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个现实了动态空间配置、空间管理、空间释放的class template。
STL六大组件的交互关系:Container通过Allocator取得数据储存空间,Algorithm通过Iterator存取Container内容,Functor可以协助Algorithm完成不同的策略变化,Adapter可以修饰或套接Functor。


STL的基本观念就是将数据和操作分离。数据由容器类别加以管理,操作则由可定制(configurable)的算法定义之。迭代器在两者之间充当粘合剂,使任何算法都可以和任何容器交互运作。如图:

5.2 容器(Containers)

总的来说,容器可分为两类:
1、序列式容器Sequence containers,此乃可序(ordered)群集,其中元素均有固定位置——取决于插入时机和地点,和元素值无关,排列次序和置入次序一致。STL提供三个定义好的序列式容器:vector,deque,list。此外你也可以讲strings(C++标准程序库中的C++类族系(basic_string<>,string,wstring))和array当做序列式容器。
2、关联式容器Associative containers,此乃已序(sorted)群集,元素位置取决于特定的排序准则和元素值,和插入次序无关。STL提供了四个关联式容器:set,multiset,map,multimap。
关联式容器也可被视为特殊的序列式容器,因为已序(sorted)群集正是根据某个排序准则排列(ordered)而成。注意,STL所提供的群集型别彼此独立,各自实现,毫无关联。
关联式容器自动对其元素排序,这并不意味它们就是用来排序的。你也可以对序列式容器的元素加以手动排序。自动排序带来的主要优点是,当你搜索元素时,可获得更佳效率。更明确地说你可以放心使用二分搜索法。

5.2.1 序列式容器

Vectors
Vector将其元素置于一个dynamic array中加以管理。它允许随机存取,也就是说你可以利用索引直接存取任何一个元素。尾部附加或移除元素非常快速(“分摊后的”高速),中部和头部比较费时,因为要移动其他元素。
所有序列式容器都提供有push_back()这个成员函数。
所有容器都提供有size()这个函数。
可以通过下标操作符[],存取vector内的某个元素。(随机存取)
Deques
所谓deque(发音类似“check”,“hack”),是“double-ended queue”的缩写。它是一个dynamic array(所以说deque也可以随机存取),可以向两端发展,在头部和尾部安插元素十分迅速,中间安插比较费时,因为要移动其他元素。
Lists
List由双向链表(doubly linked list)实现而成。这意味list内的每个元素都以一部分内存指示其前驱元素和后继元素。List不提供随机存取。一般的元素存取动作会花费“线性时间”(平均距离和元素数量呈正比)。这比vector和deque提供的“分摊性(amoritzed)”常数时间,性能差很多。
List的优势是:在任何位置执行安插和删除动作都非常迅速,因为只须改变链接(links)就行。这表示list中间位置移动元素比在vector和deque块得多。
lists并没有提供以operator[]直接存取元素的能力,因为list不支持随机存取,如果采用operator[]会导致不良效能。
Strings
你可以将string当做STL容器来使用。这里的strings是指C++string类族系(basic_string<>,string, wstring)的对象。
Arrays
另一种容器不是类别(class),而是C/C++语言核心所支持的一个型别(type):具有静态大小或动态大小的array。但array并非STL容器,它们并没有类似size()和empty()等成员函数。但STL的设计运行你针对array调用STL算法。当我们以static arrays作为初始化行(initializer list)时,这一点特别有用。
值得注意的是,我们没有必要再直接编写dynamic array了。Vectors已经具备了dynamic array的全部性质,并提供更安全更便捷的接口。

5.2.2 关联式容器

关联式容器依据特定的排序准则,自动为其元素排序。排序准则以函数形式呈现,用来比较元素值或者元素键。缺省情况下以operator进行比较,不过你也可以提供自己的比较函数,定义出不同的排序准则。
通常关联式容器由二叉树(binary tree)实现出来。具体是二叉查找树。关联式容器的差别主要在于元素的类型以及处理重复元素的方式(态度)。
Sets
Set的内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。set视为一种特殊的map:其元素实值就是键值。SGISTL实现中,set的底层机制是RB-tree这种平衡二叉搜索树,还有提供了也hash-table为底层机制的set!
Multisets
Multiset和set相同,只不过它允许重复元素。
Maps
Map的元素都是“实值/键值”所形成的一个对组。每个元素有一个键,是排序准则的基础。每一个键只能出现一次,不允许重复。Map可被视为关联式数组,也就是具有任意索引型别的数组。
Multimaps
Multimap和map相同,但允许重复。Multimap可被当做“字典”(dictionary,某种数据结构)使用。
所有关联式容器都提供一个insert()成员函数,用以安插新元素,新元素会按排序准则自动安插到正确的位置。注意,你不能使用序列式容器的push_back()和push_front()函数,它们在这里毫无意义,因为你没有权力指定新元素的位置。
一个“键值/实值”对组所形成的群集中,如果所有键值都是独一无二的,我们可将它视为一个关联式数组。Maps允许你使用operator[]安插元素,如:
map<string, float> coll;
coll["PI"] = 3.1415;
这里,以键值为索引,键值可以为任意型别。这正是关联式数组的接口。所谓关联式数组就是:索引可以采用任何型别。
Multimaps不允许使用下标操作符[],因为multimaps允许单一索引对应到多个不同元素,而下标操作符却只能处理单一实值。你必须先产生一个对组,然后再插入multimap。当然对于maps也可以这么做。存取multimaps或maps的元素时,你必须透过pair结构的first成员和second成员,才能取得键值和实值。

5.2.3 容器配接器(Container Adapter)

根据基本容器类别实作而成。
Stacks
Stack容器对元素采取LIFO(后进先出)管理策略。
Queues
Queue容器对元素采取FIFO(先进先出)管理策略。也就是说,它是个普通的缓冲区(buffer)
Priority Queues
Priority Queues容器中的元素可以拥有不同的优先级。所谓优先权,乃是基于程序员提供的排序准则(缺省使用operator<)而定义。Priority queue的效果相当于这样一个buffer:“下一个元素永远是queue中优先级最高的元素”。如果同时有多个元素具备最高优先权,则其次序无明确定义。

5.3 迭代器

迭代器是一个“可遍历STL容器内全部或部分元素”的对象。一个迭代器用来指出容器中的特定位置。基本操作:operator*(operator->)、operator++、operator==和operator!=、operator=
这种操作和C/C++“操作array元素”时的指针接口一致。每一种容器型别都必须提供自己的迭代器,各种迭代器的接口相同,型别却不同。
所有容器类别都提供成员函数begin()和end()来获得指向容器起点和终点(最后一个元素之后)的迭代器,begin()和end()形成一个半开区间。半开区间有两个优点:1、为“遍历元素,循环的结束时机”提供了一个简单的判断依据。只要尚未到达end(),循环就可以继续进行。2、不必对空区间采取特殊处理手法。空区间的begin()就等于end()。
任何一种容器都定义有两种迭代器型别:1、container::iterator(迭代器以“读/写”模式遍历元素),2、container::const_iterator(迭代器以“只读”模式遍历元素)。
注意:迭代器的“前置式递增”++pos比“后置式递增”pos++效率高。后者需要一个额外的临时变量,因为它必须存放迭代器原本位置并将它返回,所以一般情况下最好用++pos,不要用pos++。

5.3.2 迭代器分类

STL预先定义好的所有容器,其迭代器均属于以下两种类型:
1、双向迭代器(Bidirectional iterator):以递增运算前进或以递减运算后退。list、set、multiset、map和multimap这些容器所提供的迭代器都属此类
2、随机存取迭代器(Random access iterator):不但具备双向迭代器的所有属性,还具备随机访问能力。它们提供了“迭代器算术运算”必要的操作(和“一般指针的算术运算”完全对应)。你可以对迭代器增加或减少一个偏移量、处理迭代器之间的距离、或是使用 < 和 > 之类的相对关系操作符来比较两个迭代器。vector,deque和strings所提供的迭代器都属此类。
为了撰写尽可能与容器型别无关的泛型程序代码,你最好不要使用随机存取迭代器。如:
container<type> coll;
...
container<type>::iterator pos; // 使用时需要设置容器类型的
for (pos = coll.begin(); pos != eoll.end; ++pos) { // pos < coll.end()只对随机存取迭代器适合
...
}
在for循环的条件判断中使用operator!=而不要使用operator<。因为只有随机存取迭代器才支持operator<,而使用operator<作为循环判断条件时对于lists、sets和maps无法运作。但使用operator!=时需要注意pos的位置不能在end的后面。

5.4 算法

算法并非容器类别的成员函数,而是一种搭配迭代器使用的全局函数。
所有算法处理的都是半开区间——含括其实元素位置但不含括结尾元素位置,即[begin, end)!!!所以要注意其实区间不包含最后一个元素!
序列式容器可以增加容器的大小,使用resize成员函数或者构造的时候指定。

5.5 迭代器之配接器

5.5.1 Insert Iterators(安插型迭代器)

也叫inserters,可以使算法一安插(insert)方式而非覆写(overwrite)方式运作。使用它,可以解决算法的“目标空间不足”问题。会促使目标区间的大小按需要增长。
insert iterators内部将接口做了新的定义:
如果对某个元素设值,会引发“对其所属群集的安插操作”,位置由具体的insert iterator确定。
“单步前进”不会造成任何动静。
三种预先定义的insert iterators:
1. Back inserters(安插于容器的最尾端)
内部调用push_back(),只有提供有push_back()成员函数的容器中,back inserters才能派上用场。在C++标准程序库中,这样的容器有三:vector,deque,list。
2. Front inserters(安插于容器的最前端)
内部调用push_front(),只有用于提供有push_front()成员函数的容器,有deque和list。
3. General inserters(一般性安插器)
将元素插入“初始化时接受之第二参数”所指位置的前方。内部调用insert()。所有STL容器都提供有insert()成员函数,因此,这是唯一可用于关联式容器的一种预先定义的inserter(但对于关联式容器而言,这只是一种提示)。
list<int> coll1;for (int i = 1; i <= 9; ++i){coll1.push_back(i);}vector<int> coll2;copy(coll1.begin(), coll1.end(), back_inserter(coll2));for (size_t i = 0; i < coll2.size(); ++i){cout << coll2[i] << ' ';}cout << endl;deque<int> coll3;copy(coll1.begin(), coll1.end(), front_inserter(coll3));for (size_t i = 0; i < coll3.size(); ++i){cout << coll3[i] << ' ';}cout << endl;set<int> coll4;copy(coll1.begin(), coll1.end(), inserter(coll4, coll4.begin()));for (set<int>::iterator pos = coll4.begin(); pos != coll4.end(); ++pos){cout << *pos << ' ';}cout << endl;

5.5.2 Stream Iterators(流迭代器)

一种用来读写stream的迭代器。它们提供了必要的抽象性,使得来自键盘的输入像是个群集,你能够从中读取内容。同样道理,你也可以把一个算法的输出结果重新导向某个文件或屏幕上。
如istream_iterator<string>通过cin >> string来读取,ostream_iterator也类似。
vector<string> coll;// read all words from the standard inputcopy(istream_iterator<string>(cin),// start of sourceistream_iterator<string>(),// end of sourceback_inserter(coll));// destinationsort(coll.begin(), coll.end());// print all elements widthout duplicatesunique_copy(coll.begin(), coll.end(),// sourceostream_iterator<string>(cout, "\n"));// destination

5.5.3 Reverse Iterators(逆向迭代器)

所有容器都可以通过rbegin()和rend()产生出reverse iterator。

5.6 更易型算法(Manipulating Algorithms)

“删除或重排或修改元素”的算法

5.6.1 移除(removing)元素

算法remove()并没有改变群集中的元素数量,end()返回的还是当初的那个终点,size()返回的还是当初那个大小。不过需要删除的元素被删除了,实际上是被其后的元素覆盖了。事实上,这个算法返回了一个新的终点。可以利用该终点获得新区间、缩减后的容器大小,或是获得被删除元素的个数。
int main(){list<int> coll;for (int i = 1; i <= 6; ++i){coll.push_front(i);coll.push_back(i);}cout << "pre: ";copy(coll.begin(), coll.end(), ostream_iterator<int>(cout, " "));cout << endl;list<int>::iterator end = remove(coll.begin(), coll.end(), 3);cout << "number os removed elements: "<< distance(end, coll.end()) << endl; // 如果两个迭代器都是随机存取迭代器可以使用operator-直接计算距离,但list值提供双向迭代器cout << "post: ";copy(coll.begin(), coll.end(), ostream_iterator<int>(cout, " "));cout << endl;cout << "1. use new end. post: ";copy(coll.begin(), end, ostream_iterator<int>(cout, " "));cout << endl;// remove "removed" elementscoll.erase(end, coll.end());cout << "2. after erase. post: ";copy(coll.begin(), coll.end(), ostream_iterator<int>(cout, " "));cout << endl;return 0;}


透过“以迭代器为接口”,STL将数据结构和算法分离开来。然后,迭代器只不过是“容器中某一位置”的抽象概念而已。一般来说,迭代器对自己所属容器一无所知。任何“以迭代器访问容器元素”的算法,都不得(无法)透过迭代器调用容器类别所提供的任何成员函数!

5.6.2 更易型算法和关联式容器

更易型算法(移除remove、重排resort、修改modify元素的算法)用于关联式容器身上会出现问题。关联式容器不能被当做操作目标,原因很简单:如果更易型算法用于关联式容器的身上,会改变某位置上的值,进而破坏其已序特性,那就推翻了关联式容器的基本原则:容器内的元素总是根据某个排序准则自动排序。因此,为了保证这个原则,关联式容器的所有迭代器均被声明为指向常量(不变量)。如果你更易关联式容器中的元素,会导致编译错误!
需要从关联式容器中删除元素,需要调用它们的成员函数!每一种关联式容器都提供用以移除元素的成员函数。你可以调用erase()来移除元素。
set<int> coll;for (int i = 0; i <= 9; ++i){coll.insert(i);}copy(coll.begin(), coll.end(), ostream_iterator<int>(cout, " "));cout << endl;// algorithm remove() does not work// instead member function erase() worksint num = coll.erase(3);cout << "number of removed elements: " << num << endl;copy(coll.begin(), coll.end(), ostream_iterator<int>(cout, " "));cout << endl


5.6.3 算法Vs成员函数

容器本身可能提供比算法中功能相似而性能更佳的成员函数。如list的remove()成员函数。
为了避免糟糕的表现,list针对所有“更易型”算法提供了一些对应的成员函数。如果你使用list,就应该使用这些成员函数。此外要注意,这些成员函数真的移除了“被移除”的元素。如果效率是最高目标,则应该永远优先选用成员函数。

5.7 使用者自定义之泛型函数

// PrintElements()// -- prints optional C-string optcstr followed by// -- all elements of the collection coll// -- separated by spaces//template <class T>inline void PrintElements(const T& coll, const char* optcstr=""){typename T::const_iterator pos;std::cout << optcstr;for (pos = coll.begin(); pos != coll.end(); ++pos){std::cout << *pos << ' ';}std::cout << std::endl;}int main(){list<int> coll;for (int i = 1; i <= 9; ++i){coll.push_front(i);}PrintElements(coll, "list: ");return 0;}

5.8 以函数作为算法的参数

5.9 仿函数

传递给算法的“函数型参数”,并不一定得是函数,也可以是行为类似函数的对象。这种对象称为仿函数。C++中只要在类中重载操作符()就可以使这个类的对象称为一个仿函数。调用方法直接是对象括号参数,此时的对象就如同普通函数名。
仿函数较一般函数优点:
1. 仿函数是“smart functions”(智能型函数),即行为类似函数的对象。仿函数可以拥有成员函数和成员变量,这意味着仿函数拥有状态。另外你可以在执行期初始化它们——当然必须在它们被使用之前。
2. 每个仿函数都有自己的型别
3. 仿函数通常比一般函数速度快
// function object that adds the value with which it is initializedclass AddValue{private:int theValue;// the value to addpublic:// constructor initializes the value to addAddValue(int v) : theValue(v) {}// the function call for the element adds the valuevoid operator()(int& elem) const{elem += theValue;}};int main(){list<int> coll;for (int i = 0; i <= 9; ++i){coll.push_back(i);}PrintElements(coll, "initialized: ");// add value 10 to each elementfor_each(coll.begin(), coll.end(), AddValue(10));PrintElements(coll, "after adding 10: ");// add value of first element to each elementfor_each(coll.begin(), coll.end(), AddValue(*coll.begin()));PrintElements(coll, "after adding first element: ");return 0;}

5.9.2 预先定义的仿函数

标准库包含一些预先定义的仿函数,如果less<>,greater<>等,这些都是template。
通过一些特殊的函数配接器,你还可以将预先定义的仿函数和其他数值组合在一起,或使用特殊状况,如使用配接器bind2nd,将第二个参数绑定到第一个仿函数参数的第二个参数上。
另外还有一些仿函数,某些仿函数可以用来调用群集内每个元素的成员函数。如mem_fun_ref用来调用它所作用的元素的某个成员函数。

5.10 容器内的元素

5.10.1 容器元素的条件

STL容器元素必须满足以下三个基本要求:
1. 必须可透过copy构造函数进行复制。所有容器都会在内部生成一个元素副本,并返回该暂时性副本,因此copy构造函数会被频繁调用。副本和原本必须相等,且行为一致。
2. 必须可以透过assignment操作符完成赋值动作。容器和算法都使用assignment操作符,才能以新元素改写旧元素。
3. 必须可以透过析构函数完成销毁动作。当容器元素被移除,它在容器内的副本将被销毁。因此析构函数绝不能设计为private。还有析构函数绝不能抛出异常。
这三个条件对任何class而言其实都是隐式成立的,除非自己定义特殊成员。
下面几个条件,也应当获得满足:
1. 对序列式容器而言,元素的default构造函数必须可用。
我们可以在没有给予任何初值的情况下,创建一个非空容器,或增加容器的元素个数。这些元素都将以default构造函数完成。
2. 对于某些动作,必须定义operator==以执行相等测试。如果你又搜索需求,这一点特别重要。
3. 在关联式容器中,元素必须定义出排序准则。缺省情况下是operator<,透过仿函数less<>被调用。
5.10.2 Value语意VS.Reference语意
所有容器都会建立元素副本,并返回该副本。这意味着容器内的元素与你放进去的对象“相等”但非“同一”。如果你修改容器中的元素,实际上修改的是副本而不是原先对象。这意味着STL容器提供的“Value语意”。他们所容纳的是你所安插的对象值,而不是对象本身。然而实用上你也许需要用到“reference语意”,让容器容纳元素的reference。
STL只支持value语意,不支持reference语意。优点:1、元素的拷贝简单,2、使用reference容易导致错误。缺点:1、“拷贝元素”可能导致不好的效能;有时甚至无法拷贝。2、无法在数个不同容器中管理同一份对象。
虽然C++标准程序库不支持reference语意。但我们可以利用value语意来实现reference语意。可以使用指针作为元素,或者使用智能指针。不过都需要一定的技巧。不过千万不能使用C++标准程序库提供的auto_ptrs,因为它们不符合作为容器元素所需的基本要求。当auto_ptr执行拷贝或者赋值动作后,目标对象与原对象并不相等:原来的那个auto_ptr发生了变化,其值并不是被拷贝了,而是被转移了。这意味着即使对容器中的元素进行排序和打印,也会摧毁它们!所以千万别在容器内放置auto_ptr。

5.11 STL内部的错误处理和异常处理

5.11.1 错误处理

C++标准程序库指出,对于STL的任何运用,如果违反规则,将会导致未定义的行为。因此,如果索引、迭代器、或区间范围不合法,结果将未有定义。
具体来说,使用STL时,必须满足一些条件:
1、迭代器无比合法而有效。必须在使用它们之前先将它们初始。注意,迭代器可能会因为其他动作的副效应而变得无效。如vectors和deques发生元素的安插、删除或重新配置时,迭代器可能因此失效。
2、一个迭代器如果指向“逾尾”位置,它并不指向任何对象,因此不能对它调用operator*或者operator->。这一点适用于任何容器的end()和rend()所返回的迭代器。
3、区间必须是合法的
——用以指出某个区间的前后两迭代器,必须指向同一个容器
——从第一个迭代器出发,必须可以到达第二个迭代器所指位置
4、如果涉及的区间不止一个,第二区间及后继个区间必须拥有“至少和第一区间一样多”的元素。
5、覆盖动作中的“目标区间”必须拥有足够元素,否则就必须采用insert iterators。
注意,这些错误发生在执行期间而非编译期间,因而导致未定义的行为。

5.11.2 异常处理

STL几乎不检验逻辑错误。事实上,C++Standad只要求唯一一个函数调用动作必要时直接引发异常:vector和deque的成员函数at()。