Idiot-maker

  :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

翻译并且学习了A Practical Guide To STL,最后附有pdf版本的Download,算是STL的菜鸟级实用入门,疏于修正错误恐多,请多执教。

http://www.codeproject.com/vcpp/stl/PracticalGuideStl.asp

翻译:NickyYe@seuNickyYe@163.com

介绍

STL (Standard Template Library),标准模板库对于当今任何一位C++程序员来说都是一个非常优秀的技巧。我要说的是,想熟练地使用它必须付出一些努力,比如较为陡峭的学习曲线、某些不太直观的命名(可能是因为所有的好名字都已经被用完了)。好处是一旦学会他你将能避免很多令人头痛的困难。和MFC容器比起来(MFC Containers),它显得更为易用和强大。

优点:

  • 易于进行排序和搜索
  • 调试起来更加容易和安全
  • 你也可以理解经常在UNIX程序院所写的代码了
  • 可以成为加入你简历中的另一个名头

背景

这篇指南旨于让读者能够在这项极富挑战性的技术上轻松起步,而不必疲于理会成篇的术语和令人厌烦的规规矩矩,那些东西只是STL的创立者为了满足他们的消遣而制定。

代码的用处

这里的代码主要是针对STL的实践应用。

定义

  • 模板(Template 数量很多的类(以及结构、数据类型和函数)。有时候它也会被称作类似于“甜饼的模具”。从形式上看来它是范型的一种表现——一个模板类即一个范型类,一个模板函数就是一个范型函数。
  • STL - Standard Template Library,标准模板库,高手们写的模板,现在在C++标准语言中已经被每个人使用。
  • 容器(Container - 存储数据的类。在STL中有vectorssetsmapsmultimapsdeques
  • 向量(Vector - 最基本的数组模板,它是一个容器。
  • 迭代器(Iterator - 指针的另一个时髦的名字,它指向容器中的一个元素,当然它也有其他功能。

Hello World程序

我一直想写一个Hello World程序,现在正是一个价值千金的机会。下面程序所作的工作是将一个char型的字符串存储进一个character型的向量(vector)中,然后再逐字符地将它显示出来。Vector是众多数组模板中最基本的一个,STL中差不多有一半的容器都是vector的,所以如果掌握这段代码的话,在理会STL的路上你已经成功一半了。

Collapse

// 程序: Vector Demo 1

// 目的: 演示STL vectors

 

// #include "stdafx.h" –用到预编译头文件就必须include

#include <vector>  // STL vector头文件,注意没有".h"

#include <iostream>  // cout会用到它

using namespace std;  // namespace必须被置为std

 

char* szHW = "Hello World"; 

// 我们都知道,这是一个以一个null字符结束的char型数组

 

int main(int argc, char* argv[])

{

  vector <char> vec;  // 一个char型的vector (STL 数组)

 

  //为该vector定义一个迭代器——总是这样做

  vector <char>::iterator vi;

 

  // 初始化该vector,通过遍历该字符串给每个字符赋值,直到null

  char* cptr = szHW;  // 一个指向Hello World字符串的指针

  while (*cptr != '\0')

  {  vec.push_back(*cptr);  cptr++;  }

  // push_back把数据放在vector的后面 (vector的末端)

 

  // 在控制台上显示出存储在STL数组中的每个字符

  for (vi=vec.begin(); vi!=vec.end(); vi++) 

  // 这就是STL中标准的循环

  // 通常"!="被用于代替"<",因为后者在一些容器中没有被定义

  // begin()end()分别返回指向vector开始和结束的迭代器(指针)

  {  cout << *vi;  }

           // 使用间接运算符(*) iterator中提取出数据

  cout << endl;  // 不必加上"\n"

 

  return 0;

}

push_back是将数据放入vector或者deque(双向队列)中的标准函数. insertpush_back相似,它使用于所有的容器,也更为复杂。end()实际上是附加在vector最末端的一个元素,它使得循环遍历的工作能够顺利进行。它正好指向数据范围的最后,就像在常见的循环for (i=0; i<6; i++) {ar[i] = i;}中,ar[6]并不存在,也从不会遍历到,所以无关大碍。

STL 的不便之处 #1:

STL的不便首先就在于自身的声明。用数据去初始化它远比使用C/C++的数组来的困难。一般你必须逐元素的去做初始化的工作,否则就必须先初始化一个数组然后再将它赋于某个容器。我知道人们一般都这么做。

// 程序: Initialization Demo

// 姆的: 演示STL vectors的初始化

 

#include <cstring>  // <string.h>相同

#include <vector>

using namespace std;

 

int ar[10] = {  12, 45, 234, 64, 12, 35, 63, 23, 12, 55  };

char* str = "Hello World";

 

int main(int argc, char* argv[])

{

  vector <int> vec1(ar, ar+10);

  vector <char> vec2(str, str+strlen(str));

  return 0;

}

在编程的时候,还有其他很多方式去实例化一个vector,其中之一便是使用更常见的方括号,就像下面:

// 程序: Vector Demo 2

// 目的: 演示包含counter和方括号的STL vectors

 

#include <cstring>

#include <vector>

#include <iostream>

using namespace std;

 

char* szHW = "Hello World";

int main(int argc, char* argv[])

{

  vector <char> vec(strlen(sHW));

  // 参数初始化了内存区域

  int i, k = 0;

  char* cptr = szHW;

  while (*cptr != '\0')

  {  vec[k] = *cptr;  cptr++;  k++;  }

  for (i=0; i<vec.size(); i++)

  {  cout << vec[i];  }

  cout << endl;

  return 0;

}

这个例子看起来很明了,但是对于iterator的控制也少了很多,而且还多了一个整型counter,所以你必须进行显性赋值以防内存出错。

命名空间

STL密切相关的概念是命名空间(Namespaces. STL是在std的命名空间中定义的。有三种方式指定它:

  1. 在所有文件之上,头文件之下使用namespace

using namespace std;

对于简单的项目来说,这是最为简单也是最好的方法,它将你限于std的命名空间内。未来你所加的任何东西都会被当成是在std命名空间内,即使是不正确的(我想你可能要为这个开口大骂了)。

  1. 在每次使用前进行指定(类似于原型)。

  using std::cout;

  using std::endl;

  using std::flush;

  using std::set;

  using std::inserter;

这种方式略微有点冗长,对于记忆的要求比较高,你也可以较为容易地交替使用多个命名空间。

  1. 每次从std命名空间使用一个模板的时候,使用std进行定义。

typedef std::vector<std::string> VEC_STR;

这种方式非常之繁琐,但是如果你想交错使用多个命名空间的话,这恐怕是做好的办法了。一些STL的发烧友总是会这么去做,而称呼其他人为恶魔。有些人还为了简化整个过程而编写了宏。

另外,你可以将using namespace std加在任何位置,例如在函数的顶部或者在循环的内部。

一些技巧

使用下面的compiler pragma可以在调试模式下避免烦人的代码错误:

#pragma warning(disable: 4786)

另外必须知道是:在尖括号和变量名之间必须留有空格,因为>>是位操作符,所以:

vector <list<int>> veclis;

会报错,然而:

vector <list <int> > veclis;

则是正确的

另一种容器 - The set

这是微软的帮助文件对于set的解释:“一种描述对象的模板类,这个对象控制一个变长的常类型元素序列。每个元素包含一个排序序列中的键和一个值。该序列允许在对数时间内对任意元素进行查找、插入和移除的操作,即时间复杂度是序列长度的对数。另外,插入的操作不会使iterator失效,移除某个元素也仅仅会废除指向被移除元素的指针。”

另一个更为常用的定义是:set是一个存储非重复数据的容器,通常用于统计值所出现的次数。排序的方式在set实例化的时候被指定。如果数据以键/值对的方式存储的话,map会是更好的选择。Set像是一个链表,比vector在插入和移除上来得更快,但在搜索和整体求和上则稍微慢些。

下面是一个例子:

// 程序: Set Demo

// 目的: 演示STL sets

 

#include <string>

#include <set>

#include <iostream>

using namespace std;

 

int main(int argc, char* argv[])

{

  set <string> strset;

  set <string>::iterator si;

  strset.insert("cantaloupes");

  strset.insert("apple");

  strset.insert("orange");

  strset.insert("banana");

  strset.insert("grapes");

  strset.insert("grapes"); 

   // 重写

  for (si=strset.begin(); si!=strset.end(); si++) 

  {  cout << *si << " ";  }

  cout << endl;

  return 0;

}

 

// 输出:apple banana cantaloupes grapes orange

如果你想对STL掌握更深的话,你也可以用下面的代码替代程序中的输出循环。

copy(strset.begin(), strset.end(), ostream_iterator<string>(cout, " "));

虽然很有益,但我却发现上面的代码不尽清晰,易于发生错误。如果你也发现的话,就说明你已经掌握了它的用途。

其他STL容器

容器使用模板来实现,也是STL中一个重要的概念。下面是STL中实现的七类容器。

  • vector – 标准且安全的数组,只能在“前端”进行扩展。
  • deque – 功能上和vector完全一致,内部则不尽相同。在前端和尾端都可拓展。
  • list – 只能进行逐步遍历。如果你已经熟知了链表的概念的话,STL中的list使用的就是双链表,即包含指向前驱和后继的指针。
  • set – 包含已经排好序的不重复值。
  • map – 为元素排序,每个元素包含一对值,其中之一是排序和搜索所依赖的数值,它从容器中获得。比如,map可以让你执行ar["banana"] = "overripe",而不必使用ar[43] = "overripe"。所以如果你想记录每个元素的信息的话,set将能很容易实现。
  • multiset – set相同,但是其中元素的值可以重复。
  • multimap – map相同,但是其中元素的值可以重复。

注意:在阅读MFC帮助的时候,你可能会注意到各种容器的效率问题。比如,(log n * n)的插入算法时间。其实除非是处理数量极多的数值,否则这个问题都是可以忽略不计的。当然,如果涉及到对于延时和时间性非常重视的工作,你还是应该多了解一些这方面的知识。

如何在map中使用类

Map是使用键存储数值的一种模板。

现在我们提出的问题是,如何使用自己的类来代替数据类型,比如int?书写一个为模板而使用的类,必须保证它包含一些特殊函数,基本如下:

  • 默认构造函数(通常为空)
  • 拷贝构造函数
  • 重载 "="

因为模板要求的不同,更多的运算符可能被要求重载。比如,如果你是为map准备一个类的话,关系运算符的重载就是必要的。这是后话。

Collapse

// 程序: Map Own Class

// 目的: 演示类在map中的使用

 

#include <string>

#include <iostream>

#include <vector>

#include <map>

using namespace std;

 

class CStudent

{

public :

  int nStudentID;

  int nAge;

public :

  // 默认构造函数

  CStudent()  {  }

  // 完整的构造函数

  CStudent(int nSID, int nA)  {  nStudentID=nSID; nAge=nA;  }

  // 拷贝构造函数

  CStudent(const CStudent& ob) 

    {  nStudentID=ob.nStudentID; nAge=ob.nAge;  }

  // 重载 =

  void operator = (const CStudent& ob) 

    {  nStudentID=ob.nStudentID; nAge=ob.nAge;  }

};

 

int main(int argc, char* argv[])

{

  map <string, CStudent> mapStudent;

 

  mapStudent["Joe Lennon"] = CStudent(103547, 22);

  mapStudent["Phil McCartney"] = CStudent(100723, 22);

  mapStudent["Raoul Starr"] = CStudent(107350, 24);

  mapStudent["Gordon Hamilton"] = CStudent(102330, 22);

 

  // 通过名字来访问

  cout << "The Student number for Joe Lennon is " <<

    (mapStudent["Joe Lennon"].nStudentID) << endl;

 

  return 0;

}

TYPEDEF

下面是typedef使用的一个例子:

typedef set <int> SET_INT;

typedef SET_INT::iterator SET_INT_ITER

通常的惯例是使用大写字母和下划线来命名。

ANSI / ISO 字符串(string

ANSI/ISO strings(字符串)在STL容器中使用很广泛,它是你的标准字符串类。除了形式不佳的声明外,它得到了广泛的赞誉。 你需要使用<<iostream中的相关内容(decwidth等)来处理你的字符串。

需要的时候可以使用c_str()来获得一个指向character的指针。

迭代器(Iterators

上面提到过iterators其实就是指针,但是还包含更多内容。他们看上去像指针,同指针一样工作,然而实际上却包含在间接运算符* (一元)和->的重载中,用来从容其中返回一个值。存储iterator随意长的时间是不可取的,因为当一个值被加入到容器中或者从容器中移除的话,它常常会失效。从这个意义上来说,迭代器和句柄有些类似。迭代器简单且易于改变,所以我们可以用多种方式遍历整个容器:

  • Iterator(迭代器) 除了vector的其他所有容器,每次你只能向前逐步遍历。也就是说你只能使用++运算符,而不是+=。只是对于vector 你可以使用+= ---=++中的任意一个,以及所有的比较运算符,比如<<=>>===!=
  • reverse_iterator(逆向迭代器) - 在除了vector以外的容器中,用reverse_iterator代替iteratorrbegin()代替begin()rend()代替end(),使用++的时候就会进行逆向遍历了。
  • const_iterator(常量迭代器) 返回一个值的正向遍历器,用于指针指向只读的值。
  • const_reverse_iterator(常量逆迭代器) -返回一个值的逆向遍历器。

SetsMaps中的排序

模板除了值的类型外还有其他参数。同样,你可以使用回调(callback)函数(断言,只有一个参数且返回值为bool型的函数)。例如,你想要一个顺序排序的stringset,按下列方式声明:

set <int, greater<int> > set1

greater <int>是一个用于存储数值得模板函数(范型函数),它被置于容器中。如果你想set按照逆向排序的话:

set <int, less<int> > set1

在很多情况下,你都必须将一个谓词作为参数传向STL类,就像下面的算法所描述的。

STL 的不便之处 #2 – 冗长的错误信息

模板化的命名因为编译器而冗长,因而当编译器遇到某些错误停止时,就会报出长得难以阅读的错误信息。对于这一点我至今还没有找到好的解决方法。最好还是培养在报错的地方寻找错误的能力。相关的不便还有,当双击模板错误的时候,它会将你带到相应的指针,而这同样难以读懂。有时候,最好的解决方法是反复仔细检查你的代码,完全忽视编译器的报错信息。

算法

算法就是模板实现的功能,也是STL强大功能的真正体现之处。只要了解一些常用的函数名,你就可以掌握大部分容器的使用,安心地进行排序、查找、操作和交换。通常他们的使用都会有一个范围,例如:sort(vec.begin()+1, vec.end()-1)就是针对除了最首和最末位的元素进行的排序操作。

容器本身不会被传向算法,仅仅是指明范围的两个迭代器(iterators)。这样,算法就不会被容器所直接限制,而是某个算法所支持的iterators。另外,很多时候一个特别准备的函数名也同样要被作为参数传递(前面提到的断言),甚至是普通的数值也是可以的。

实际中算法应用的例子

Collapse

// 程序: Test Score

// 目的: 演示关于vector的考试分数的算法应用

 

#include <algorithm>

   // 算法的头文件.

#include <numeric>  // (For Accumulate)

#include <vector>

#include <iostream>

using namespace std;

 

int testscore[] = {67, 56, 24, 78, 99, 87, 56};

 

// 判断是否通过考试的断言

bool passed_test(int n)

{

  return (n >= 60);

}

 

// 判断是否未通过的断言

bool failed_test(int n)

{

  return (n < 60);

}

 

int main(int argc, char* argv[])

{

  int total;

  // 给考试分数的vector赋值初始化

  vector <int> vecTestScore(testscore,

     testscore + sizeof(testscore) / sizeof(int));

  vector <int>::iterator vi;

 

  // 排序并显示vector

  sort(vecTestScore.begin(), vecTestScore.end());

  cout << "Sorted Test Scores:" << endl;

  for (vi=vecTestScore.begin(); vi != vecTestScore.end(); vi++)

  {  cout << *vi << ", ";  }

  cout << endl;

 

  // 显示数据

 

  // min_element返回范围中最小的_iterator_

  // 所以*运算符被用来提取值

  vi = min_element(vecTestScore.begin(), vecTestScore.end());

  cout << "The lowest score was " << *vi << "." << endl;

 

  // 对于max_element,同上

  vi = max_element(vecTestScore.begin(), vecTestScore.end());

  cout << "The highest score was " << *vi << "." << endl;

 

  // 用断言函数来判断通过与否

  cout << count_if(vecTestScore.begin(), vecTestScore.end(), passed_test) <<

    " out of " << vecTestScore.size() <<

    " students passed the test" << endl;

 

  // 未通过

  cout << count_if(vecTestScore.begin(),

    vecTestScore.end(), failed_test) <<

    " out of " << vecTestScore.size() <<

    " students failed the test" << endl;

 

  // 分数之和s

  total = accumulate(vecTestScore.begin(),

     vecTestScore.end(), 0);

  // 显示平均分

  cout << "Average score was " <<

    (total / (int)(vecTestScore.size())) << endl;

 

  return 0;

}

Allocator(分配符),等会儿见

Allocator在模板的初始化阶段被使用到,他们是外表背后真正神秘的部分。除非你做的是高级的内存优化工作,否则还是不要注意它。通常情况下,不必去指定他们,因为他们已经被作为默认的参数处理。最好还是等他们自己出现的时候再去研究。

派生模板和嵌入模板

STL类可以使用正常类的任何使用方式。

它可以是嵌入的:

class CParam

{

  string name;

  string unit;

  vector <double> vecData;

};

也可以作为基类使用:

class CParam : public vector <double>

{

  string name;

  string unit;

};

派生必须谨慎使用,只要是符合你的编程风格即可。

嵌套模板

为了声明一个更为复杂的数据结构,你可以嵌套使用模板。最好的方式是实现使用typedef 定义内模板,因为显然后面它将被用到。

Collapse

// 程序: Vector of Vectors Demo

// 目的: 演示嵌套STL模板容器

 

#include <iostream>

#include <vector>

 

using namespace std;

 

typedef vector <int> VEC_INT;

 

int inp[2][2] = {{1, 1}, {2, 0}}; 

  // 普通的2x2数组,将被置于模板中

int main(int argc, char* argv[])

{

  int i, j;

  vector <VEC_INT> vecvec;

  // 一步完成的代码如下:

  // vector <vector <int> > vecvec;

 

  // 用前面的数组赋值

  VEC_INT v0(inp[0], inp[0]+2);  // 传递两个指针

    // 将被拷贝到vector中的数值的范围

  VEC_INT v1(inp[1], inp[1]+2);

 

  vecvec.push_back(v0);

  vecvec.push_back(v1);

 

  for (i=0; i<2; i++)

  {

    for (j=0; j<2; j++)

    {

      cout << vecvec[i][j] << "  ";

    }

    cout << endl;

  }

  return 0;

}

 

// 输出:

// 1 1

// 2 0

尽管初始化的工作略显繁琐,你却可以得到一个无限的2*2数组空间,直到内存耗尽。同样可以根据要求适用到其它容器的组合中。

结论

STL非常实用,但是也有烦人的地方。就像中国人所说的那样:学会它后你就将与虎添翼。

相关链接

历史

  • 2004323提交
  • 200442修正

关于Jeff Bogan

I shuffle tiny static charges on intricate wafers of silicon all day.

点击here浏览Jeff Bogan的在线信息

posted on 2007-02-08 23:40  NickyYe  阅读(482)  评论(1编辑  收藏  举报