技术开发 频道

体验Visual C++.NET 2005中的STL

  

  定义一个公共基础

  深入了解并使用STL.NET的方法有两种:一种途径是可以通过了解STL和STL.NET的区别来掌握。另外一种途径是通过了解他们有哪些共性。虽然那些有STL丰富使用经验的人来说似乎需要一张“区别清单”,然而这并不能使你从那些不熟悉的库的乌烟瘴气中逃离出来,所以说,当我们放慢脚步,深入这些或那些容器以及他们是如何与系统集合库(System collection libraries)互操作等这样的深奥角落的时候—虽然这些都是非常整洁优雅的,但我们直接的去了解这些新的内容不是更好么?至少在下面的简短介绍部分我希望大家是这样做的。通过这种方法,对于那些新手来说就可以对STL和STL.NET提供的参数化集合的扩展模型有一个了解。

  那么STL和STL.NET都有哪些共性呢?这两者都包括两个主要的组件:顺序式(Sequential)和关联式(associative)容器类型,以及一个泛型算法集。(是的,如果你是一个经验丰富的STL开发者,你就应当知道我下面要讨论什么。同样,两者具有同样的术语和背景,所以你得耐心点。) 泛型算法不直接操作容器类型。取而代之的是,我们使用了一个迭代器来成对的标识用于操作的元素范围。元素范围符号(正式的术语为左包含区间)如下所示:

// 读作:包含first直至但是不包括last

[ first, last )

  这表明了范围是从first开始以last结束但是不包含last。当first等于last的时候这个范围就是空了。

  顺序式容器中存储了单独类型元素的序列集。vector和list类型是两个主要的顺序式容器。(第三个顺序式容器是deque—发音deck—提供了vector同样的功能,但是用于对高效插入和删除第一个元素这种特殊情况。一般我们首选deque而不是vector,例如一个队列的实现。)

  我们在访问一个连续容器类型之前,我们必须包含进合适的头文件。这些头文件同时也包含了共享基类接口的声明,例如interface_vector,以及泛型在容器中的应用,例如generic_vector。

  STL.NET的容器是引用类型的;容器的声明中会有一个追踪句柄,它会自动初始化为nullptr。我们通过gcnew操作符分配一个实际的容器。我们在前面的小节中曾经简单地提到过,现在我来详细的解释一下:

  void f()
  {
    //分配一个空向量(vector)……
    vector^svec = gcnew vector;
    //分配一个含有10个元素的表(list)
    //每一个值都默认都被设为nullptr
    list^olist = gcnew list(10);
    //追踪句柄值自动设为nullptr
    deque^ideck;
    //做一些有趣的事情
  };

  关联式容器(associative container)支持对现有元素的显示和检索的高效查询。关联式容器类型中最主要的两个就是map和set。Map是一个key(键)/value(值)对:key用于查询,value用于存储和检索数据。举个例子来说,电话号码簿可以用map简单地表示出来:key是每一个人的名字,value是相关联的电话号码。

  map使用带下划线的树抽象来按照值升序排列条目。hash_map容器能够进行更为高效的检索排序。尽管如此,hash_map迭代也是某种程度上随机地访问元素。如果检索是map中的主要活动,就应首选hash_map容器。

  set包含一个单键值(key value) 并且支持元素是否存在的高效查询。例如,文本查询系统在建立文本词库的时候可能需要建立一个常用词语集并将之排除在文本外,例如the, and, but, 等等。程序将轮流的读取文本中的每一个词,检查它是否是拒绝接纳词汇集中的词汇,根据查询的结果,或者是忽略这个词,或者是将之存入数据库。除了set以外,还有hash_set 容器,它与map和hash_map有相同的特性。

  map和set只能包含一个键(key)。multimap和multiset 支持出现多个同样的键。还以我们的电话号码簿为例,我们可能需要将一个人列在多张表中。在这种情况下,我们就要使用multimap。同样也存在hash_multimap和hash_multiset。

  与这些容器类型相关联的头文件如下所示:

//用于map和multimap
#include
//用于set和multiset
#include
//用于hash_map和hash_multimap
#include
//用于hash_set和hash_multiset
#include 

  一个简单的演示

  让我们通过一个实例来具体地讨论一下。下面的例子实现了文本词汇统计。它展示了map, hash_set, 和vector的使用。函数声明如下:

map^

build_word_count( vector^>^ words,

array ^common_words )

  模板语法让人看起来非常地复杂。让我们看看能够做哪些有益的改进。build_word_count() 的返回类型是map,键(key)是字符串—文本中每一个不同于其他词的单词—值(value)是用于统计相关联词出现次数的整数。函数的第一个参数是CLI数组向量(vector),它将文件中的词汇保存为字符串元素。第2个参数中保存着我们要排除在外的字符集。

  由于很多“帽子”和右中括号间隔出现,这看起来相当的复杂。我们可以使用typedef从文字上化简一下:

typedef array^ words;
typedef vector^ text;

  使用typedef重定义的函数如下所示:

map^
build_word_count( text theText, words common )

  我们也可以除去显式的map声明,但是出于读者练习的考虑我仍然保留了声明。下一个工作是实现。我们可以将之分为两部分:(1) map和hash_set的初始化,如下所示,(2)实际的文本处理程序。

  //第1项:初始化容器
  //分配一个空map……我们并不清楚它的大小……
  map^wordOccur = gcnew map;
  //用数组中的元素填充hash_set
  hast_set^comWords = gcnew hast_set(&common[0],&common[common->Length]);

  如果你不熟悉STL,hash_set的初始化可能会使你感到陌生。但是除了语法之外,它的声明是非常直接和有力的。

  我们要做的是使用数组中的元素来初始化hash_set。我们显然可以做到,当然要用一个for each语句进行迭代实现。例如,

  //简化hash_set声明
  //第一部分:分配一个空的hash_set
  hash_set^comWords = gcnew hash_set;
  //第二部分:用数组中的元素填充hash_set
  for each(String^wd in common)
  comWords->insert(wd);

  数组元素中的第一个直至有效的最后一个元素的地址装入hash_set的构造函数为元素装入hash_set提供了同等的机会。这两个地址为hash_set 构造函数的迭代提供了一个元素范围,轮流的将它们插入容器中。这就是我在这部分开始提到的迭代器的范围。

  实现的第2部分便是文本处理。因为我们还没有看到官方正式发布的迭代器,我们现在仍然使用for循环来遍历vector。我们将使用for each语句来遍历vector的每一个数组元素。这就是如上所述的代码:

//循环的访问每一个数组

for ( int ix = 0; ix < theText->size(); ++ix )
{
 for each ( String^ wd in theText[ ix ] )
  if ( common->find( wd ) == common->end() )
   word_map[ wd ]++;
}

// 不要忘记返回结果……
return word_map;

  find() 是hash_set的成员函数,它用于确定词汇项是否已经存在于set当中—所有的关联式容器都支持find()成员函数。(是的,顺序式容器,以及内置数组,和所有你可能将集成到STL/STL.NET模型中的集合,都使find()泛型算法。算法和容器的隔离理论上比实际上要为严重。我们将特别地讨论一下列表顺序式容器(list sequential container) 。)他返回的是我还没有讨论过的—容器中的迭代器。此时,我们将迭代器假想为指针类型。如果在容器中词汇项已经存在了,那么find()将返回给它一个迭代器;否则,将返回最后一个词汇项的迭代器。这与end()返回的迭代器值是相同的。我们判断STL下的元素搜索是否成功的方法就是比较用搜索方法返回的迭代器值与end()方法返回的迭代器值是否相同。如果两者相同,那么元素便不在容器中。

  每一个容器都提供begin()和end()成员函数。begin()返回给容器中first元素一个迭代器。正如我前面说到的,end()返回给容器中last元素一个迭代器。举例如下,下面的程序中展示了我们如何声明两个迭代器并且用这两个成员函数初始化它们,

vector::iterator first = theText->begin();
vector::iterator last = theText->end();

  容器的遍历一般使first在一个for循环或者是while循环中递增,当first等于last的时候终止循环。例如,

for ( ; first != last; ++first )

  对迭代器对的要求是他们必须能够通过增量运算符的重复应用以first开头并能够达到last。尽管如此,编译器自身不能做到;如果不能够达到这个要求则会产生未定义的运行时行为(undefined run-time behavior)。

  我们通过反引用(*)操作符访问迭代器涉及到的元素。例如,为了重新获得我们的向量(vector)的每一个CLI数组元素,我们在上述的for循环体中写入下面的代码,

array ^as = *first;

  算法的选择

  泛型算法为这些容器类型和两个内置数组类型的元素提供了操作。这些操作包括Search (find, count), Sort (merge, partition, permutate, reverse, rotate, shuffle, sort), Deletion/Subtitution (remove, replace, swap, unique), Copy, Relational (equal, min, max, includes), Generate (fill, for-each, generate, transform), Set (union, intersection, difference), Heap (make, sort, pop, push), 以及很多深奥的数字操作,例如积分,部分求和,内积(inner product),和临差(adjacent difference)。

  在我们使用泛型算法前,必须包含进相应的头文件。

  让我们看看如何使用这些算法。举例来说,让我们在将字符串元素数组插入map之前对它们进行排序。我们将使用sort()泛型算法来实现它。对于所有的泛型算法,参数几乎都是迭代器对构成的范围:

sort( &as[0], &as[ as->Length ] );

  在开始的代码中还要使用到另外两个泛型算法—find()和remove():

//泛型算法:find

vector::iterator iter = find( svec->begin(), svec->end(), "Pooh" );

if ( iter != svec->end() ){ ... }

//泛型算法:remove……

remove( svec->begin(), svec->end(), "Rabbit" );

  到现在为止我们应当对这里的用法模式非常敏感了:由迭代器对划分出的范围与容器的算法密不可分。搜索算法返回一个迭代器,或者用于发现容器中的项,抑或是找不到,迭代器用于标识范围内的最后一个项。

0
相关文章