1、抽象容器类型顺序容器(sequence container)拥有由单一类型元素组成的一个有序集合。两个主要的顺序容器是 list 和 vector。第三个顺序容器为双端队列 deque,发音为“deck” ,它提供了与vector 相同的行为,但是对于首元素的有效插入和删除提供了特殊的支持。例如,在实现队列时(队列是一种抽象,用户每次总是获取第一个元素) ,deque 比 vector 更为合适。关联容器(associative container)支持查询一个元素是否存在,并且可以有效也获取元素。两个基本的关联容器是 map(映射)和 set(集合)。Map 是一个键 /值(key/valu
2、e)对;键(key) 用于查询,而值(value)包含我们希望使用的数据。例如, map 可以很好地支持电话目录,键是人名,值是相关联的电话号码。Set 包含一个单一键值,有效支持关于元素是否存在的查询。例如,当我们要创建一个单词数据库,且它包含在某个文本中出现的单词时,文本查询系统对能会生成一个单词集合以排除 the, and 以及 but 等等。程序将顺次读取文本中的每个单词,检查它是否属于被排除单词的集合,并根据查询的结果将其丢弃或者放入数据库中。Map 和 set 都只包含每个键的唯一出现,即每个键只允许出现一次.multimap(多映射)和multiset(多集合 )支持同一个键的多
3、次出现。例如,我们的电话目录可能需要为单个用户支持多个列表 ,一种实现方法是使用 multimap.我们的文本查询系统文本查询系统应该由什么构成呢?1 用户指定的任意文本文件。 2 一个逻辑查询机制,用户可以通过它查询文本中单词或相邻的单词序列。如果一个单词或相邻的单词序列被找到,则程序显示出该单词或单词序列的出现次数。如果用户希望的话,则包含单词或单词序列的句子也会被显示出来。例如,如果用户希望找到所有对 Civil War 或 Civil Rights 的引用,则查询可能如下:Civil cout svec;svec.reserve( 32 ); / 把容量设置为32/ .使 svec 的
4、长度为 0(即 0 个元素) ,而容量为 32。但是,根据经验发现,用一个非 1 的缺省值来调整 vector 的容量看起来会引起性能退化。例如,对于 string 和 double 型的vector,通过 reserve()增加容量导致很差的性能。另一方面,增加大型复杂类的容量,会大大改善了性能,如表所示调整容量时插入 10000 元素的时间容量 时间(s)缺省值 1 6.704096 5.558192 4.4410000 2.22注:非简单类:8000 字节大小,并且带有构造函数和析构函数对于我们的文本查询系统,将使用一个 vector 来包含 string 对象,并且使用与它相关联的缺省
5、容量。虽然当我们插入未知数目的 string 时,vector 会动态增长,但是正如前面显示的计时情况来看,它的性能仍然比 list 好一些。定义一个顺序容器为了定义一个容器对象,我们必须包含相关联的头文件,应该是下列头文件之一:#include #include #include #include #include 容器对象的定义以容器类型的名字开始,后面是所包含的元素的实际类型,例如:vector svec;list ilist;定义了 svec 是一个内含 string 对象的主 vector,以及 ilist 是一个 int 型对象的空 list。svec和 ilist 都是空的。为了
6、确认这一点,我们可以调用 empty()操作符。例如:if ( svec.empty() != true ); / 喔, 有错误了插入元素最简单的方法是 push_back(),它将元素插入在容器的尾部。例如:string text_word;while ( cin text_word )svec.push_back( text_word );每次从标准输入读取一个字符串放到 text_word 中。然后 push_back()再将 text_word 字符串的拷贝插入到 svec 中。list 和 deque 容器也支持 push_front(),它把新元素插入在链表的前端。例如,假设我们有
7、一个 int 型的内置数组如下:int ia 4 = 0, 1, 2, 3 ;用 push_back():for ( int ix = 0; ix #include #include extern int get_word_count( string file_name );const int list_size = 64;list ilist( list_size );vector svec(get_word_count(string(“Chimera“);容器中的每个元素都被初始化为“与该类型相关联的缺省值” 。对于整数,将用缺省值 0 初始化所有元素。对于 string 类,每个元素都将
8、用 string 的缺省构造函数初始化。除了用相关联的初始值来初始化每个元素外,我们还可以指定一个值,并用它来初始化每个元素。例如:list ilist( list_size, - 1 );vector svec( 24, “pooh“ );除了给出初始长度外,我们还可以通过 resize()操作重新设置容器的长度。例如,当我们写下面的代码时:svec.resize( 2 * svec.size() );我们将 svec 的长度加了一倍。每个新元素都被初始化为“与元素底层类型相关联的缺省值”。如果我们希望把每个新元素初始化为某个其他值,则可以把该值指定为第二个参数:/ 将新元素初始化为pigl
9、etsvec.resize( 2 * svec.size(), “piglet“ );那么,svec 的原始定义的容量是多少?它的初始长度是 24 个元素。它的初始容量可能是多少?对-svec 的容量也是 24。一般地, vector 的最小容量是它的当前长度,当 vector 的长度加倍时,容量一般也加倍。我们也可以用一个现有的容器对象初始化一个新的容器对象。例如:vector svec2( svec );list ilist2( ilist );每个容器支持一组关系操作符,我们可以用来比较两个容器,这些关系操作符分别是:等于、不等于、小于、大于、小于等于,以及大于等于。容器的比较是指两个容
10、器的元素之间成对进行比较。如果所有元素相等而且两个容器含有相同数目的元素,则两个容器相等。否则,它们不相等。第一个不相等元素的比较决定了两个容器的小于或大于关系。例如,下面是一个程序的输出,它比较了五个 vector:ivec1: 1 3 5 7 9 12ivec2: 0 1 1 2 3 5 8 13ivec3: 1 3 9ivec4: 1 3 5 7ivec5: 2 4/ 第一个不相等元素: 1, 0/ ivec1 大于 ivec2ivec1 ivec2 / trueivec3 ivec1 / trueivec5 ivec2 / true我们能够定义的容器的类型有三个限制(实际上,它们只适用
11、于用户定义的类类型):- 元素类型必须支持等于操作符- 元素类型必须支持小于操作符- 元素类型必须支持一个缺省值(对于类类型,即指缺省构造函数)所有预定义数据类型,包括指针,都满足这些限制,C+标准库给出的所有类型也一样。迭代器迭代器(iterator) 提供了一种一般化的方法,对顺序或关联容器类型中的每个元素进行连续访问。例如,假设 iter 为任意容器类型的一个 iterator,则:+iter;向前移动迭代器,使其指向容器的下一个元素,而:*iter;返回 iterator 指向元素的值。每种容器类型都提供一个 begin()和一个 end()成员函数。- begin()返回一个 ite
12、rator,它指向容器的第一个元素。- end()返回一个 iterator,它指向容器的末元素的下一个位置。为了迭代任意容器类型的元素,我们可以这样写:for ( iter = container.begin();iter != container.end(); +iter )do_something_with_element( *iter );由于模板和嵌套类语法的原因,iterator 的定义看起来有点吓人。例如,下面是一对iterator 的定义,它们指向一个内含 string 元素的 vector:/ vector vec;vector:iterator iter = vec.beg
13、in();vector:iterator iter_end = vec.end();iterator 是 vector 类中定义的 typedef。以下语法:vector:iterator引用了 vector 类中内嵌的 iterator typedef, 并且该 vector 类包含 string 类型的元素。为了把每个 string 元素打印到标准输出上,我们可以这样写:for( ; iter != iter_end; +iter )cout void even_odd( const vector *pvec,vector *pvec_even,vector *pvec_odd )/ 必须
14、声明一个const_iterator, 才能够遍历pvecvector:const_iterator c_iter = pvec-begin();vector:const_iterator c_iter_end = pvec-end();for ( ; c_iter != c_iter_end; +c_iter )if ( *c_iter % 2 )pvec_odd-push_back( *c_iter );else pvec_even-push_back( *c_iter );最后,如果我们希望查看这些元素的某个子集,该怎么办呢?如每隔一个元素或每隔三个元素,或者从中间开始逐个访问元素。我们
15、可以用标量算术运算使 iterator 从当前位置偏移到某个位置上。例如:vector:iterator iter = vec.begin()+vec.size()/2;将 iter 指向 vec 的中间元素,而:iter += 2;将 iter 向前移动两个元素。Iterator 算术运算只适用于 vector 或 deque,而不适用于 list,因为 list 的元素在内存中不是连续存储的。例如:ilist.begin() + 2;是不正确的,因为在 list 中向前移动两个元素需要沿着内部 next 指针走两次。对于 vector或 deque,前进两个元素需要把当前的地址值加上两个元
16、素的长度。容器对象也可以用“由一对 iterator 标记的起始元素和末元素后一位置之间的拷贝”来初始化。例如,假设我们有:#include #include #include int main()vector svec;string intext;while ( cin intext )svec.push_back( intext );/ process svec .我们可以定义一个新的 vector 来拷贝 svec 的全部或部分元素。int main()vector svec;/ ./ 用svec 的全部元素初始化svec2vector svec2( svec.begin(), svec
17、.end() );/ 用svec 的前半部分初始化svec3vector:iterator it =svec.begin() + svec.size()/2;vector svec3( svec.begin(), it );/ 处理 vectors .用特定的 istream_iterator 类型,我们可以更直接地向 svec 插入文本元素:#include #include #include int main()/ 将输入流iterator 绑定到标准输入上istream_iterator infile( cin );/ 标记流结束位置的输入流iteratoristream_iterato
18、r eos;/ 利用通过cin 输入的值初始化svecvector svec( infile, eos );/ 处理 svec除了一对 iterator 之外,两个指向内置数组的指针也可以被用作元素范围标记器(range marker) 。例如,假设我们有下列 string 对象的数组:#include string words4 = “stately“, “plump“, “buck“, “mulligan“;我们可以通过传递数组 words 的首元素指针和末元素后一位置的指针来初始化 string vectorvector vwords( words, words+4 );第二个指针被用作
19、终止条件,它指向的对象(通常指向容器或者数组中最后一个元素后面的位置上)不包含在要被拷贝或遍历的元素之中。类似地,我们可以按如下方式初始化一个内含 int 型元素的 list:int ia6 = 0, 1, 2, 3, 4, 5 ;list ilist( ia, ia+6 );顺序容器操作Push_back()方法给出了 “在顺序容器尾部插入单个元素 ”的简短表示。但是,如果我们希望在容器的其他位置插入元素,该怎么办呢?或者,如果我们希望在容器的尾部或某个其他位置插入一个元素序列,该怎么办呢?在这些情况下,我们将使用一组更通用的插入方法。例如,为了在容器的头部插入元素,我们将这样做:vecto
20、r svec;list slist;string spouse( “Beth“ );slist.insert( slist.begin(), spouse );svec.insert( svec.begin(), spouse );这里在,insert()的第一个参数是一个位置(指向容器中某个位置的 iterator),第二个参数是将要被插入的值,这个值被插入到由 iterator 指向的位置的前面。更为随机的插入操作可以如下实现。string son( “Danny“ );list:iterator iter;iter = find( slist.begin(), slist.end(),
21、son );slist.insert( iter, spouse );这里在,find()返回被查找元素在容器中的位置,或者返回容器的 end() iterator, 表明这次查询失败。Push_back() 是下列调用的简短表示:/ 等价于: slist.push_back( value );slist.insert( slist.end(), value );insert()方法的第二种形式支持“在某个位置插入指定数量的元素 ”。例如,如果希望在vector 的开始处插入 10 个 Anna,我们可以这样做:vector svec;.string anna( “Anna“ );svec.i
22、nsert( svec.begin(), 10, anna );insert()方法的最后一种形式支持“向容器插入一段范围内的元素 ”。例如,给出下列 string数组:string sarray4 = “quasi“, “simba“, “frollo“, “scar“ ;我们可以向字符串 vector 中插入数组中的全部或部分元素。svec.insert( svec.begin(), sarray, sarray+4 );svec.insert( svec.begin() + svec.size()/2,sarray+2, sarray+4);另外,我们还可以通过一对 iterator 来
23、标记出待插入值的范围,可以是另一个 string 元素的vector:/ 插入svec 中含有的元素/ 从svec_two 的中间开始svec_two.insert( svec_two.begin() + svec_two.size()/2,svec.begin(), svec.end() );或者,更一般的情况下,也可以是任意一种 string 对象的容器。list slist;/ ./ 把svec 中含有的元素插入到/ slist 中stringVal 的前面list:iterator iter =find( slist.begin(), slist.end(), stringVal );
24、slist.insert( iter, svec.begin(), svec.end() );删除操作删除容器内元素的一般形式是一对 erase()方法:一个删除单个元素,另一个删除由一对iterator 标记的一段范围内的元素。删除容器末元素的简短方法由 pop_back()方法支持例如,为了删除容器中一个指定的元素,我们可以简单地调用 erase(),用一个 iterator 表示它的位置。在下列代码段中,我们用 find()泛型算法获得待删除元素的 iterator,如果 list中存在这样的元素,则把它的位置传递给 erase():string searchValue( “Quasim
25、odo“ );list:iterator iter =find( slist.begin(), slist.end(), searchValue );if ( iter != slist.end() )slist.erase( iter );要删除容器中的所有元素,或者由 iterator 对标记的子集,我们可以这样做。/ 删除容器中的所有元素slist.erase( slist.begin(), slist.end() );/ 删除由iterator 标记的一段范围内的元素list:iterator first, last;first = find( slist.begin(), slist
26、.end(), val1 );last = find( slist.begin(), slist.end(), val2 );/ . 检查first 和last 的有效性slist.erase( first, last );最后,如同在容器尾部插入一个元素的 push_back()方法相仿,pop_back()方法删除容器的末元素-它不返回元素,只是简单地删除它。例如:vector:iterator iter = buffer.begin();for ( ; iter != buffer.end(); iter+ )slist.push_back( *iter );if ( ! do_some
27、thing( slist )slist.pop_back();赋值和对换当我们把一个容器类型赋值给另一个时,会发生什么事情?赋值操作符使用针对容器元素类型的赋值操作符,把右边容器对象中的元素依次拷贝到左边的容器对象中。如果两个容器的长度不相等,又会怎么样呢?例如:/ slist1 含有10 个元素/ slist2 含有24 个元素/ 赋值之后都含有24 个元素slist1 = slist2;赋值的目标,在本例中为 slist1, 它现在拥有与被拷贝容器相同的元素数目。Slist1 中的前10 个元素被删除。Swap()可以被看作是赋值操作符的互补操作。当我们写:slist1.swap( sli
28、st2 );时,slist1 现在含有 24 个 string 元素,是用 string 赋值操作符拷贝的,就如同我们写:slist1 = slist2;一样。两者的区别在于,slist2 现在含有 slist1 中原来含有的 10 个元素的拷贝。如果两个容器的长度不同,则容器长度就被重新设置,且等于被拷贝容器的长度。泛型算法从概念上度,我们的思想是把所有容器类型的公共操作抽取出来,形成一个通用算法集合,它能够被应用到全部容器类型以及内置数组类型上。这组通用算法被称作泛型算法。泛型算法通过一个 iterator 对,被绑定到一个特殊的容器上。例如,下面的代码显示了我们怎样在一个 list,ve
29、ctor,以及不同类型的数组上调用 find()泛型算法:#include #include int ia 6 = 0, 1, 2, 3, 4, 5 ;vector svec;list dlist;/ the associated header file#include vector:iterator viter;list:iterator liter;int *pia;/ 如果找到, find()返回指向该元素的iterator/ 对于数组, 返回指针pia = find( liter = find( dlist.begin(), dlist.end(), some_double_value
30、 );viter = find( svec.begin(), svec.end(), some_string_value );因为 list 容器类型不支持随机访问其元素,所以它提供了额外的操作,如 merge()和 sort()。存储文本行我们的第一个任务是读入用户需要查询的文本文件。需要获得下列信息:每个单词,当然,还有每个单词的位置,-即,它在哪一行,以及在该行的位置。而且,为了显示出与一个查询相匹配的文本行,我们必须按行号保留文本。怎样获得文本的每一行呢?标准库支持 getline()函数,声明如下:istreamgetline()读取 istream 对象,向 string 对象插入
31、字符,包括空格,直到遇到分割符、文件结束,或者被读入的字符序列等于 string 对象的 max_size()值,在该点处读入操作失败。在每次调用 getline()之后,我们都会将 str 插入到代表文本的字符串 vector 中。下面是一般化的实现。我们已经将它提取到一个函数中,命名为 retrieve_text()。为了增加被收集到的信息,我们定义了一对值来存储最长行的行数和长度。/ 返回值是指向string vector 的指针vector*retrieve_text()string file_name;cout file_name;/ 打开文本文件以便输入 .ifstream inf
32、ile( file_name.c_str(), ios:in );if ( ! infile ) cerr *lines_of_text =new vector;string textline;typedef pair stats;stats maxline;int linenum = 0;while ( getline( infile, textline, n ) cout push_back( textline );linenum+;return lines_of_text;程序的输出如下:please enter file name: alice_emmaline read: Alice
33、 Emma has long flowing red hair. Her Daddy saysline read: when the wind blows through her hair, it looks almostalive,line read: like a fiery bird in flight. A beautiful fiery bird, hetells her,line read: magical but untamed. “Daddy, shush, there is no suchthing,“line read: she tells him, at the same
34、 time wanting him to tell hermore.line read: Shyly, she asks, “I mean, Daddy, is there?“number of lines: 6maximum length: 66longest line: like a fiery bird in flight. A beautiful fiery bird,he tells her,由于每个文本行都是作为 string 被存储的,所以我们需要把每行拆成独立的单词。对于每个单词,我们将首先去掉标点符号。例如,考虑下面“For every tale theres a telli
35、ng,and thats the he and she of it.“它产生下列单独的 string,这些 string 可能带有内嵌的标点符号。“Fortherestelling,thatsit.“这些 string 需要被变成:Fortheretellingthatit有人可能会说:theres应该变成there is但是实际上我们打算走向另一个方向:我们将丢弃没有语义的单词,如 is, that, and, it 以及the 等等。因此只有taletelling我们将用一个专门排除单词的 set 来实现这一点除了支持标点符号外,我们还需要去掉大写字母,并提供对后缀的最小处理。大写字母在下
36、列文本行中成为一个问题。Home is where the heart is.A home is where they have to let you in.显然,关于 home 的查询需要找到两次。对于后缀的处理是要解决一个更为复杂的识别问题,例如,识别 dog 和 dogs 表示相同的名词,love,loves 以及 loving 表示同一动词。找到一个子串我们的第一个任务是将表示文本行的字符串分解成独立的单词。我们将通过找到内嵌的空格来达到这个目的。例如,给出:Alice Emma has long flowing red hair.通过标记出其中的六个空格,我们能识别出七个子字符串,它
37、们表示了文本行中实际的单词。为了做到这一点,我们使用 string 类支持的多个 find()函数之一。String 类提供了一套查找函数,都以 find 的各种变化形式命名。Find()是最简单的实例:给出一个字符串,它返回匹配子串的第一个字符的索引位置。或者返回一个特定的值:string:npos表明没有匹配。例如:#include #include int main() string name( “AnnaBelle“ );int pos = name.find( “Anna“ );if ( pos = string:npos )cout #include int main() stri
38、ng numerics( “0123456789“ );string name( “r2d2“ );string:size_type pos = name.find_first_of( numerics );cout #include int main() string numerics( “0123456789“ );string name( “r2d2“ );string:size_type pos = 0;/ 这里存在错误!while ( pos = name.find_first_of( numerics, pos )!= string:npos )cout words;while (
39、 pos = textline.find_first_of( , pos )!= string:npos )words.push_back( textline.substr(prev_pos, pos-prev_pos);prev_pos = +pos;Substr()操作生成现有 string 对象的子串的一个拷贝。它的第一个参数指明开始的位置。第二个可选的参数指明子串的长度(如果省略第二个参数,将拷贝字符串的余下部分) 。我们的实现有个错误;在插入每一行的最后一个单词时,它就会失败。你知道为什么吗?考虑下面的文本行:seaspawn and seawrack前两个单词由空格标记。这两个空格
40、的位置都由 find_first_of()返回。但是,第三次调用并没有找到空格符,它将 pos 置为 string:npos,结束了循环。那么,最后一个单词的处理必须跟在循环结束之后。下面是完整的实现,我们已将其放入一个被称为 separate_words()函数中。除了把每个单词存储在字符串 vector 中之外,我们还计算了每个单词的行列位置。typedef pair location;typedef vector loc;typedef vector text;typedef pair text_loc;text_loc*separate_words( const vector *tex
41、t_file )/ words: 包含独立单词的集合/ locations: 包含相关的行/列信息vector *words = new vector;vector *locations = new vector;short line_pos = 0; / current line number/ iterate through each line of textfor ( ; line_pos size(); +line_pos )/ textline: 待处理的当前文本行/ word_pos: 文本行中的当前列位置short word_pos = 0;string textline = (
42、*text_file) line_pos ;string:size_type pos = 0, prev_pos = 0;while ( pos = textline.find_first_of( , pos )!= string:npos )/ 存储当前单词子串的拷贝words-push_back(textline.substr( prev_pos, pos - prev_pos );/ 将行/列信息存储为pairlocations -push_back(make_pair( line_pos, word_pos );/ 为下一次迭代修改位置信息+word_pos; prev_pos = +
43、pos;/ 现在处理最后一个单词words-push_back(textline.substr( prev_pos, pos - prev_pos );locations-push_back(make_pair( line_pos, word_pos );return new text_loc( words, locations );到现在为止,我们的程序的控制流如下:int main()vector *text_fo;e = retroeve_text);text_loc *text_locations = separate_words( text_file );/ .Separate_wo
44、rds()在 text_file 上的部分执行情况如下:eol: 52 pos: 5 line: 0 word: 0 substring: Aliceeol: 52 pos: 10 line: 0 word: 1 substring: Emmaeol: 52 pos: 14 line: 0 word: 2 substring: haseol: 52 pos: 19 line: 0 word: 3 substring: longeol: 52 pos: 27 line: 0 word: 4 substring: flowingeol: 52 pos: 31 line: 0 word: 5 sub
45、string: redeol: 52 pos: 37 line: 0 word: 6 substring: hair.eol: 52 pos: 41 line: 0 word: 7 substring: Hereol: 52 pos: 47 line: 0 word: 8 substring: Daddylast word on line substring: says.textline: magical but untamed. “Daddy, shush, there is no such thing,“eol: 60 pos: 7 line: 3 word: 0 substring: m
46、agicaleol: 60 pos: 11 line: 3 word: 1 substring: buteol: 60 pos: 20 line: 3 word: 2 substring: untamed.eol: 60 pos: 28 line: 3 word: 3 substring: “Daddy,eol: 60 pos: 35 line: 3 word: 4 substring: shush,eol: 60 pos: 41 line: 3 word: 5 substring: thereeol: 60 pos: 44 line: 3 word: 6 substring: iseol:
47、60 pos: 47 line: 3 word: 7 substring: noeol: 60 pos: 52 line: 3 word: 8 substring: suchlast word on line substring: thing,“.textline: Shyly, she asks, “I mean, Daddy, is there?“eol: 43 pos: 6 line: 5 word: 0 substring: Shyly,eol: 43 pos: 10 line: 5 word: 1 substring: sheeol: 43 pos: 16 line: 5 word:
48、 2 substring: asks,eol: 43 pos: 19 line: 5 word: 3 substring: “Ieol: 43 pos: 25 line: 5 word: 4 substring: mean,eol: 43 pos: 32 line: 5 word: 5 substring: Daddy,eol: 43 pos: 35 line: 5 word: 6 substring: islast word on line substring: there?“在加入我们的文本查询函数集合之前,先简要地概括一下 string 类支持的其他查找函数。除了 find()和 fin
49、d_first_of()外,string 类还支持其他几个查找操作: rfind(),查找最后(即最右)的指定子串的出现。例如:string river( “Mississippi“ );string:size_type first_pos = river.find( “is“ );string:size_type last_pos = river.rfind( “is“ );find()返回索引值 1,表明第一个“is ”的开始,而 rfind()返回索引值 4,表明“is”的最后一个出现的开始。Find_first_not_of()查找第一个不与要搜索字符串的任意字符相匹配的字符。例如,为找到第一个非数字字符,可以写:string elems( “0123456789“ );string dept_code( “03714p3“ );/ returns index to the character pstring:size