首页 > 学院 > 开发设计 > 正文

轻轻松松学STL

2019-09-10 09:07:01
字体:
来源:转载
供稿:网友

 一个新标准的出现,对于熟习旧环境的工程师而言,总是有近乡情怯之感。明知新标准一定比旧标准来的好,但对于旧标准总是有那么一点依依不舍,对新标准也总是有些许隔阂,虽然几个月后就不这么想了。

 

 C++ STL程式库的出现,对于习惯使用Borland C++等等程式库的工程师而言,也有类似的感觉。有人说:C++就是把C语言那些令人既爱又恨的程式技巧,予以标准化,成为语言的一部份;STL则是把C++那些令人期待盼望的功能,予以公开化,让大家不用花大钱买程式库就可使用!而且使用STL,不必担心日后程式码会有大更动,因为毕竟这是ANSI C++的标准。所以本文将从STL概念上的基本原则出发,带您在复杂的STL程式库中找寻自己的认知天地

 如果您对STL起源与基本结构有兴趣,可以参考上期STL的简介,或是到下面这个WWW站参考一下最新资讯,里面有ANSI C++通过的最新STL版本等等资料。

http://www.cs.rpi.edu/~musser/stl.html

 

STL对编译器要求

 每当我们学习一个新的程式库,通常第一个会问的,就是如何在我的编译器上,写一个简单的程式,证明它可以跑。STL对编译器的要求很严格,以最普遍的Borland C++ 4.5为例,Borland C++必须在程式码前加一段:

 

#define __MINMAX_DEFINED

#pragma option -vi-

 

才可以跑程式,并且include的路径也要设对。而微软的Visual C++ 4.0因为不支援DOS模式下的程式,如果要简化GUI的处理来使用STL,最简单的方式便是在Console Application模式下写程式,而且include的路径也要设对,如此才可以跑。至于Visual C++2.x版本的编译器,因为template机制做的不好,并不能编译STL程式库。值得注意的是,这里所说的「可以跑程式」,并不代表所有STL的功能都可以使用,因为C++ template机制过于复杂,可以有很多种变化,现今的编译器技术无法正确的实作,所以现在用STL所写的程式,日后或多或少还是需要修改,但已经比以往使用专属程式库改版时,需要做的修改来得少很多。

STL的基本概念:Container,Iterator,Algorithm

 在学习STL之前,让我们介绍STL最基本的概念,如图一:演算法物件透过Iterator操作各种容器物件,完成运算。而除了基本的演算法由STL内建提供外,工程师可以自己定义一些新的辅助演算法(help function,或称辅助函数),来完成新的计算。这些新的辅助演算法,通常是利用C++的template function的方式设计。您可能会问,依照OO理论对于物件的定义,为什么称STL的演算法物件为「物件」,它其实只是函数而已?答案是:因为C++中的函数名称事实上也是一种型别,利用typedef可将之宣告成指向函数指标的型别,所以当我们以C++中的型别为准,enum、union、struct、typedef等等关键字都可看做是一种类别的变形宣告,也因此可以把单一函数看做是物件(没有资料成员的物件)。

 图一左边的容器物件,STL则定义三个最基本也最常用到的的容器物件:vector,deque,list。而其他各种资料结构教科书上定义的特定资料结构,如stack, heap, binary tree等等,都可以再利用这三个基本的容器,加以变化,实作之。基本上,图一左边的物件,STL利用C++的template class制作。

 

 

 

     图一、STL的根本概念

 

 

 所以有些人(如[Nel95]书)就把STL的功能分成五大部份,掌管图一从左至右大大小小的事情,如图二:

 

  图二、STL的五个部份[Nel95]

 演算法物件,Iterator物件,容器物件仍在,STL的Adaptor物件包装了由基本容器物件所产生的变形资料结构,如stack,queue,priority_queue等;Function物件则辅助演算法物件完成一些更为基本的运算,如比较大小等。以下,我们将以图一的原则审视STL的各组成部份。

Container

 Container顾名思义,就是一种容器,里头可以装各式各样的物件。如下,

 

template <class T>

class container {

/t T d;

};

 

透过template<class T>,T可表示任意物件所属的类别,于是在容器类别内产生一个T类别的物件d。由于T可以表示各种资料型别,不管是C++内建的基本资料型别或是利用class定义的类别,都可以存入STL的容器物件中。

 STL将容器物件分为两种,循序式容器(Sequence Container)与关系式容器(Associate Container)。循序式容器内的每一个元素,只能包含一种物件,且各元素的排列顺序完全依照元素插入时的顺序。关系式容器内的每一个元素,则包含两种物件,一个为键物件(key object);一个为值物件(value object),且各元素的排列顺序完全依照键物件决定。

循序式容器

循序式容器又可分成四种基本的容器类别,C语言指标中的记忆体(您可想象其为一个巨大的阵列)、vector(向量)、deque(双向伫列)、list(串列),一个简单的vector容器使用如下:

 

// test program for vector container

#include <iostream.h>

 

 

#include <vector.h>

void main() {

/t vector<char*> v;

/t v.push_back("me");

/t v.push_back("you");

/t v.push_back("he");

/t v.push_back("she");

/t for(int i=0;i<4;i++)

/t/t   cout<<v<<"";

}

//end程式输出

me

you

he

she

 

 vector<data_type>表示我们可以放入各种资料型别到<...>之中,vector物件便会正确的产生空间存放此型的物件。v.push_back(object)函数则把属于该型别的物件存入容器物件的记忆空间中。

 一个vector容器好比一个传统C语言中的阵列,只是传统C语言中的阵列必须明确地指出阵列大小,如果注标值超过边界值,则系统将得到一个未知的错误。然而,STL的vector容器却不然,vector容器自己视需要,自动加大阵列的大小,所以应用程式就不需担心注标值超过阵列范围。图示如三。

 start表示阵列起始处,finish表示阵列结束处,end_of_storage表示实际阵列结束的地方,如果阵列仍持续成长,超过end_of_storage,则vector容器会自动在配置一块记忆体,维持注标值不超过边界值。

 其他STL的基本容器简介如下:

1.deque容器如同资料结构中的双向伫列,单向伫列具有FIFO的性质,双向伫列则更进一步可以由两边存入资料。

 

图三、vector容器的记忆体结构

2.list容器为一个双向串列,可以实作树等资料结构。

3.C语言中的记忆体则为最传统的容器物件,因为记忆体也可以储存各种型态的物件,只要透过适当的C语言指标指向之即可。

关系式容器

 关系式容器,STL共定义了set<Key>、multiset<Key>、map<Key,T>、multimap<Key,T>四种容器物件,其主要特色为:「物件带有关键值」。每种容器的特色如下:

 

Key可重复吗?

容器有资料成员吗

set<Key>

multiset<Key>

map<Key,T>

multiset<Key,T>

 关系式容器让C++程式可以处理关系式资料。关系式资料是指一组资料由键值与资料成员所组成。如同资料库对资料表格的定义,STL的关系式容器中的键值可以选择是否重复,不可重复的关系式容器好比数学中的集合概念,集合内的资料皆是唯一性的。由于这些容器在存入资料成员时都会把新的资料成员依键值做排序,所以您可想象成好比一个有序的表或集合,甚至关系式容器也容许资料成员是空的,容器只储存键值,并做排序而已。

 一个简单使用map容器的程式如下:

 

//work for Borland C++ only

#include <iostream.h>

#include <iomanip.h>

#include <cstring.h>

#include <map.h>

void main() {

/t map<string,int,less<string>> m;

/t m["me"]=1;

/t m["you"]=2;

/t m["he"]=3;

/t m["she"]=4;

/t cout<<setw(3)<<m["she"]<<setw(3)<<m["he"]<<setw(3)

/t/t   <<setw(3)<<m["you"]<<setw(3)<<m["me"];

}

 

 

//程式输出

 4  3  2  1

 

 less<string>为一个比较类别,目的在于如何在众多物件中(string物件),知道哪一个物件优先权高、哪一个低。而键物件是指“me”、“you”等字串,资料成员物件是指1、2、3、4等整数物件。

 由上表,我们知道关系式容器的每个元素最多可含两种物件。只含有键物件的统称set类物件;含有键物件与值物件的统称map类物件。而每类物件又可依其是否可含重复键,分成single物件与multi物件。这些物件事实上都是资料结构中的树状结构。每个容器在存入物件时,便依键物件的大小决定它在树中的位置。而less<string>是后段提到的Function物件,在此先略过不谈。

 关系式容器的功用还可加以扩大,如同资料库程式对于关连式表格的应用,STL关系式容器可以很直觉地对应的关连式资料库中的表格,再搭配下期将介绍的「扩充STL的Allocator物件」,便可使您的C++程式拥有简单的永存机制(persistence),成为物件导向资料库系统(OODB)。

Iterator

 Iterator物件之于容器物件,好比指标之于记忆体。外部程式透过Iterator,可以在不知容器物件内部情况之下,操作容器物件,进而控制容器内装的物件,参考图一。STL很多地方均倚赖Iterator才能实现「效率」与「泛型」两个主要目标。使用Iterator后,演算法可以实作于各种物件、各种容器之上,只要该容器提供相对应的Iterator物件即可。

 整个STL Iterator物件可以分为五类,如图四:各种Iterator物件简介如下:

Input

输入Iterator。负责从指定串列流读入资料,如同档案I/O般。

图四、Iterator的种类

Output

输出Iterator。负责输出物件于指定的串列

流,如同档案I/O。

以上两个Iterator只是单纯地负责物件输入与输出的工作,在整个Iterator阶层中显得较不重要。

Forward

如同C的指标,具有operator*()的功能,并限制Iterator进行的方向永远往前(就是提供operator++()的功能),不能跳格。为最有用的Iterator。

Bidirectional

同Forward Iterator,并提供operator--()的功能。STL循序式容器物件皆支援此Iterator,可以说Bidirectional Iterator可操作的容器物件最多。但是,就是因为支援的容器物件较多,所以执行速度比Random Access Iterator慢。

Random Access

最强大的Iterator,几乎与真实的指标一样,也提供operator[]()的功能,但支援的容器物件只有vector容器物件与deque容器物件而已,list容器物件只支援Bidirectional Iterator。

 图三所以成金字塔型,因为支援最上层Random_Access_Iterator的容器物件与演算法物件最少;最下层的Input or Output Iterator,则几乎所有的STL容器演算法物件都支援,应用范围最为广大。

 举个程式如下:

 

// test program for iterator

#include <iostream.h>

#include <deque.h>

void main() {

/t deque<int> q;

/t q.push_back(1);

/t q.push_back(2);

/t q.push_back(3);

/t q.push_back(4);

/t for(deque<int>::iterator i=q.begin();

/t/t   i != q.end(); i++)

/t/t   cout<<*i<<endl;

 

 

}

 

 deque<int>容器在定义时给定其储存int型别的物件,存入一些int物件后,我们想要浏览之。宣告deque<int>::iterator i,表示i为deque定义的Iterator,想象i为一个指标,游走于deque容器之中,如要取得容器内int物件值时,使用*i便可。q.begin()、q.end()为传回deque容器的开始与结束的指标。

 到此,体会一下演算法物件如何透过Iterator操作容器物件。您可想象这里的for回圈为演算法物件,只要输入q.begin()、q.end()便可完成将容器内之值输出的工作。以下,我们正式介绍演算法物件。

Algorithm与Function Object

 STL提供的演算法均是最基本的演算,如排序、收寻演算法等。演算法操作Iterator物件,再透过Iterator物件操作容器物件,便达到演算法与容器物件无关化。

 整个STL定义的演算法大致可分为七类,简介如下:

Non-mutating sequence algorithms

此类演算法只适用于循序式容器,并且执行完后不会改变容器内的值。如find()、count()函数等。

Mutating sequence algorithms

此类演算法只适用于循序式容器,并且执行完后会改变容器内的值。如copy()、swap()、fill()函数等。

Sorting and searching algorithms

就如同其名字一样,此类演算法执行排序与收寻的工作,只适用于循序式容器,因为关系式容器必定是排序好的,因此不需要此类函数。

Set operations

此类演算法适用于所有STL定义的容器物件,执行集合的运算,如set_union()、set_intersection()、set_difference()函数等。

Heap operations

此类演算法很像堆积资料结构中的运算,如push_heap()、pop_heap()函数等,与Adaptor容器物件很像,只是一个为资料结构定义其资料(Adaptor);一个为资料结构定义其操作函数(如xx_heap()函数等)。

Numeric algorithms

此类演算法执行一般简单的数值计算功能,如accumulate()、partial_sum()函数等。

Other functions

不属于上面分类的其他演算法,如min()、max()函数等。

 以下我们以排序与收寻类的演算法为例,说明STL中的演算法物件如何与Iterator和容器物件互相工作。

 

// test program for sort algorithm

#include <stdlib.h>

#include <iostream.h>

#include <algo.h>

#include <vector.h>

class INT {

public:

/t INT() {}

 

 

/t INT(int a) { d=a; }

/t INT(const INT& a) { d=a.d; }

/t INT& operator=(const INT& a) { d=a.d;return *this; }

/t int operator<(const INT& a) { return d<a.d; }

/t operator int() const  { return d; }

/t int d;   };

int my_comp(const INT& a,const INT& b) {

/t return a< b;   }

void main() {

/t int i;

/t vector<INT> v;

/t cout<<"The original vector...";

/t for(i=0;i<10;i++) {

/t/t   v.push_back(rand());

/t/t   cout<<v<<" ";

/t }

/t sort(v.begin(),v.end(),my_comp);

/t cout<<"After sorting ...";

/t for(i=0;i<10;i++)

/t/t   cout<<v<<" ";

/t cout<<endl;

}

 

   宣告一个vector<INT>表示容器物件,10个元素值,而sort函数的原型如下:

tenplate <class RandomAccessIterator,class Compare> void sort(RandomAccessIterator first,RandomAccessIterator last,Compare comp);传进两个RandomAccessIterator类的Iterator后,与一个函数指标型别,sort函数自动帮您做排序,所用的演算法为qsort,平均排序时间为NlogN。

 您可发现,STL所定义的演算法物件几乎都以template function出现,输入的参数则是Iterator类的物件,从函数原型上更可看出「泛型化程式库」的精神,演算法以抽象概念操作各种型态的物件

 不过,对于int my_comp(INT&,INT&)函数,在sort演算法物件中对应的宣告为template <class Compare comp>,sort内的定义则使用comp(*first,*last)来执行此函数的功能。可以想见,今天我们定义一个INT型态的物件,需要一个比较INT物件大小的函数(如本例中的my_comp),如果以后定义一个X型别的物件,也必须为其定义一个比较大小的函数,而这个函数竟然只是执行一道指令:

 

return a<b;

 

它依靠X物件对于操作元<的过荷定义,呼叫X物件的operator<(X& x)函数完成实际功能,从这牵扯到三个步骤:

1.X物件必须为operator<()函数做定义,

2.如此一来,my_comp函数才可以单纯地执行return a<b;就可,

3.在sort演算法物件内,才可以简单地呼叫comp(*first,*last),知道谁大谁小。

 上述三步骤,如果以执行效率来看,第一步骤与第三步骤由于可以采用C++的inline机制,做巨集展开,几乎不会花执行时间。可是第二步骤虽然最单纯,只有一道指令,但由于是以函数指标的方式传进sort的函数内,透过函数指标执行此函数,执行时间自然比较慢;而且,我们必须为每一个型别(包括C++内建型别与自行定义的类别)做这个「小函数」,尽管它的功能如此简单。

 STL针对这个问题,再次应用了template的技巧,来帮忙演算法物件执行低阶的工作。如下:

// test program for Function Object

#include <stdio.h>

#include <stdlib.h>

#include <iostream.h>

#include <vector.h>

#include <algo.h>

#include <function.h>

class INT {

public:

/t INT() {}

/t INT(int a) { d=a; }

/t INT(const INT& a) { d=a.d; }

/t INT& operator=(const INT& a) { d=a.d;return *this; }

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表

图片精选