标准模板库 和 泛型编程

 

三、标准模板库

 

STL提供了一组表示容器、迭代器、函数对象及算法的模板。其中:

容器:与数组类似,存储相同类型的多个值。

算法:完成特定任务的方法。

迭代器:遍历容器的对象,是广义指针。

函数对象:类似于函数的对象,可以是类对象函数指针。

 

1、模板类vector

在很久以前我们就学习了有关vector的内容,它使用<type>表示法指出类型;同时使用动态分配,因此可以用初始化参数指出需要多少元素。

#include<vector>

using namespace std;
vector<int> ratings(5);
int n;
cin>>n;
vector<double> scores(n); //a vector of n doubles

   分配器(Allocator)

  与string类相似,各类STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器管理内存。以vector为例:

template <class T, class Allocator = allocator<T>>
     class vector{}

若省略该模板参数的值,将默认使用allocator<T>类,它使用new和delete。

 

2、可对vector执行的操作

首先,所有的STL容器提供了一些基本方法:

   size()返回容器中元素数目、

   swap()交换两个容器的内容、

   begin()返回一个指向容器中第一个元素的迭代器、

   end()返回一个表示超过容器尾的迭代器。

迭代器(iterator):是一个广义指针:它即可以是指针,也可以是一个对其执行类似指针的操作,如解除引用operator*()和递增operator++()——的对象。通过将指针广义化为迭代器,让STL能够为各种不同的容器类提供同一的接口。

在每个容器内中,都定义了一个合适的迭代器,类型是一个名为iterator的typedef,作用域为整个类。

同样以vector容器为例:

vector<double>::iterator pd;
vector<double> scores;
pd = scores.begin();
*pd = 22.3;
++pd;

或不必这么麻烦:

vector<double> scores;

auto pd = scores.begin();

超过结尾(past-the-end):指向容器最后一个元素后面的那个元素。类似于C风格字符串末尾的'\0'。

既然我们已得知begin和end的含义,即可遍历容器:

for(pd = scores.begin(); pd != scores.end(); pd++) cout<<*pd<<endl;

 

同样,vector模板类也包含一些只有它能做到的事情:

push_back():将元素添加到矢量的末尾,同时它还负责内存管理。

erase():删除矢量中给定区间的元素。它接受两个迭代器参数(学CAD的:这我熟(shou)!)

insert():接受3个迭代器参数,第一个参数指定新元素插入的位置,第二个和第三个参数定义里被插入区间——该区间通常是另一个容器对象的一部分。

使用如下:

vector<double> scores;
double temp;
while(cin>>temp && temp >= 0)
    scores.push_back(temp);
cout<<"You entered "<<scores.size()<<"scores.\n";

scores.erase(scores.begin(), scores.begin() + 2 );

vector<int> old_v;
vector<int> new_v;
old_v.insert(old_v.begin(), new_v.begin() + 1, new_v.end());

 

3、对矢量可执行的其他操作

STL为了方便,对这些模板定义了非成员函数来执行搜索、排序等操作——它不属于任何一个模板,但能为任何属于STL的模板所用,并且两个不同的容器类可以通过非成员函数进行操作如swap(),即便它们不是同样的容器类对象。同时,它还对部分模板类定义了特殊的成员函数,因为这样可能会提高效率。

以下有三个STL非成员函数():

for_each():接受三个参数,前两个是定义容器中区间的迭代器,第三个是指向函数的指针,或函数对象。for_each()函数将被指向的函数用于区间内的各个元素。

使用:

vector<Review>::iterator pr;
for(pr = books.begin(); pr!=books.end();pr++)
   ShowReview(*pr);

for_each(books.begin(),books.end(),ShowReview);

可避免显式使用迭代器变量。

 

random_shuffle():接受两个用来指定区间的迭代器参数,并随机排列该区间中的元素。例:

rand_shuffle(books.begin(), books.end());

 

sort():有两个版本的重载,其一接受两个用来定义区间的迭代器参数,并使用为存储在容器的类型元素定义的<运算符,对区间中的元素进行比较;

其二是使用由程序员定义的operator<()的重载函数,它对于某一特定的对象进行排序。

两个版本的使用如下:

vector<int> coolstuff;
sort(coolstuff.begin(), coolstuff.end());

bool operator<(const Review & r1, const Review & r2)
{
   if(r1.title < r2.title) return true;
   else if(r1.title == r2.title && r1.rating < r2.rating) return true;
   return false;
}

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

4、基于范围的for循环(C++11)

基于范围的for循环是为STL设计的:

double price[5] = {4.99, 10.99, 6.87, 7.99, 8.49};
for(double x:prices) cout<<x<<std::endl;

若并不知道数组元素的类型,可使用auto:

for_each(books.begin(), books_end(), ShowReview);

//可改为:

for(auto x: books)ShowReview(x);

 

四、泛型编程

STL是一种泛型编程(generic programming)。

1、为何使用迭代器

在此之前,我们先了解为两种不同的数据结构实现find函数:
double:

double* find_ar(double* ar, int n, const double & val)
{
   for(int i = 0; i < n; i++)
      if(ar[i] == val) return &ar[i];
   return 0;
}

对于某种Node:

struct Node
{
  double item;
  Node*  next;
}

Node* find_ll(Node * head, const double & val)
{
   Node * start;
   for(start = head; start!=0; start = start->p)
      if(start->item == val)return start;
   return 0;
}

简单地看,这两个Find例程并没有多少相关联的地方——一个是通过数组下标遍历,另一个是通过链表遍历。但从广义上来看,它们都通过遍历寻找需要的元素,所以在广义上二者相同。同时,迭代器的使用就是为了仅使用一个Find例程来处理各种容器类型。

要实现这样的一个迭代器,必须具备以下功能:

1、能够对迭代器执行解除引用(指针)操作,以便能访问它引用的值。

2、应该能够将一个迭代器赋给另一个迭代器,若p、q均为迭代器,则需要对 q = p进行定义。

3、能比较两个迭代器,看它们是否相等,若p、q都是迭代器,则应对 q == p以及q!= p定义。

4、能够使用迭代器遍历容器中的所有元素——可通过定义++p和p++来实现。

显然,常规的指针就能满足迭代器的要求,具体的相应Find实现如下:

 

对于数组:

typedef double* iterator;

iterator find_ar(iterator ar; int n, const & val)
{
   for(int i = 0; i < n; i++, ar++)
      if(*ar == val) return ar;
   return null;
}

iterator find_ar(interator begin, interator end, const double & val)
{
   for(ar = begin; ar != end; ar++)
      if(*ar == val)return ar;
   return null;
}

对于Node结点,我们可以通过定义一个类,在类中重载*和++。

struct Node
{
  double item;
  Node *next;
}

class iterator
{
  Node * pt;
public:
  iterator():pt(0){}
  iterator(Node * pn):pt(pn){}
  double operator*() {return pt->item;}
  iterator& operator++() //for ++it
  {
     pt = pt->next;
     return *this;
  }
  iterator operator++(int)
  {
    iterator tmp = *this;
    pt=pt->next;
    return tmp;
  };

注意:此处的 iterator 并非创建的一个链表,而仅仅是单一的Node,因此,这时的this指针便可指向该对象,同时又由于重载了各种运算符,因此可对其进行相应的操作。

在拥有了这样的类后,便可以编写这样的find函数:

iterator find_ll(iterator head, const double & val)
{
   iterator start;
   for (start = head; start!=0; ++start)
       if(*start == val)
          return start;
   return 0;
}

这样,find_ar()和find_ll()操作的差别仅在于循环结束:前者是使用超尾元素,而后者使用NULL指针。若将链表最后再添加一个超尾元素,这样二者将无异。

 

STL遵循的便是上述的方法,每个容器类(vector、list、deque)定义了相应的迭代器类型。同时,每个类都拥有超尾标记,将在迭代器到达容器的最后一个元素后将其赋给迭代器。

2、迭代器类型

在部分的容器中,它需要迭代器重载+运算符,以便访问容器中两个不相邻的元素,进而对容器类的所有元素进行排序。

因此,STL定义了5中迭代器,并根据所需的迭代器类型对算法进行描述:

输入迭代器    输出迭代器    正向迭代器    双向迭代器     随机访问迭代器

下面以find和sort为例:

template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T & value);

template<class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator Last);

同时,这五种迭代器都能执行解除引用操作,或进行比较。

   1、输入迭代器

 “输入”是从程序的角度来说的:即对于程序来说,来自容器内的信息被视为输入,因此输入迭代器可被程序用来读取容器内的信息。即:对输出迭代器解除引用能够使程序读取容器内的值,但不一定能让程序修改值。

  同时,假设输入迭代器连续两次遍历容器时,它并不保证容器内元素的顺序不发生改变,同时,输入迭代器被递增后,也不能保证其先前的值仍然能被解除引用。因此,基于输入迭代器的算法应该为单通行(single-pass)——即不依赖上一次遍历时迭代器的值,也不依赖你本次遍历中前面迭代器的值。

  即:输入迭代器是单项迭代器,可以递增,但不能倒退。

   2、输出迭代器

与输入相对,输出迭代器指将信息从程序传给容器的迭代器。它只是解除引用,让程序能够修改容器值,而不能读取。我们以cout为例打个比方:cout可以修改发送到显示器的字符流,却不能从屏幕上读取内容,输入迭代器也是类似的原理。若算法不用读取容器的内容就能修改它,则没有理由要求它能使用读取内容的迭代器。

即:对于单通道、只读算法,可使用输入迭代器,对于单通道,只写算法,可用输出迭代器。

   3、正向迭代器

正向迭代器只使用++遍历容器,并按照相同的顺序,同时,将正向迭代器递增后,仍能对上次的迭代器解除引用(只要保存了它),并可以得到相同的值。

正向迭代器既可以读取和修改数据,也可以使得智能读取数据。

   4、双向迭代器

它拥有正向迭代器的所有特性,并且同时支持++和--运算符。

   5、随机访问迭代器

有些算法要求能够跳到容器中的任意一个元素(如:归并排序、二分检索)。则需要随机访问迭代器,它拥有双向迭代器的所有特性,并添加了支持随机访问的操作。

 

3、迭代器层次结构 详见书P690,图16.4

使用这么多迭代器的目的是为了在编写算法时尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。

例:find()可用于任何包含可读取值的容器;

而   sort()由于需要随机访问迭代器,因此智能用于支持这种迭代器的容器。

 

4、概念、改进和模型

虽然可以设计具有正向迭代器特征的类,但不能让编译器算法限制为只使用这个类,因为正向迭代器是一系列要求而非类型。STL算法可以使用任何满足其要求的迭代器实现。相关的文献用概念(concept)描述一系列要求,若容器类需要迭代器,则可考虑STL,它包含用于标准种类的迭代器模板。

同时,概念也有类似于继承的关系,如:双向迭代器继承了正向迭代器的功能,然而却不能将继承机制用于迭代器,如:可以将正向迭代器实现为一个类,双向迭代器实现为一个指针。因此,相关文献使用改进(refinement)描述这种关系。

概念的具体实现被称作模型(model)。

 

    1、将指针用作迭代器

迭代器是广义的指针,而指针满足所有迭代器的要求。迭代器是STL算法的接口,而指针是迭代器,因此STL算法可以使用指针来对基于指针的非STL容器进行操作。

如:可将STL算法用于数组:假设Receipts是double类型的数组,并按升序对它进行排序,则有:

const int SIZE = 100;
double Receipts[SIZE];
sort(Receipts, Receipts + SIZE);

由于指针是迭代器,同时C++允许常规数组的超尾行为,因此以上代码成立并对Receipts进行排序。

 

     copy()、ostream_iterator和istream_iterator

copy()的前两个迭代器参数表示要复制的范围,最后一个迭代器参数表示要将第一个元素复制到什么位置。

otream_iterator是输出迭代器概念的一个模型,也是一个适配器(adapter)——一个类或函数,可将一些其他接口转换为STL使用的接口。

#include<iterator>
ostream_iterator<int, char> out_iter(cout, " ");

其中,第一个模板参数int指出被发送给输出流的信息类型,第二个模板参数char指出输出流使用的字符类型,也可使用wchar_t。

在模板生成的构造函数out_iter()的参数列表中,cout指出要使用的输出流,最后一个字符串参数指出发送给数据项后显示的分隔符。

要注意,这时生成的out_iter不仅是个构造函数,还是一个迭代器,因此可以这样使用:

*out_iter++ = 15;

copy(dice.begin(), dice.end(), out_iter);

copy(dice.begin(), dice.end(), ostream_iterator<int, char>(cout, " ") ); //ok!

其中,通过第二种方式创建的是一个匿名迭代器,用后即将销毁。

iterator头文件还定义了一个istream_iterator模板,使istream输入可用作迭代器接口,它是一个输入迭代器概念的模型,可以使用两个istream_iterator定义copy的范围,具体使用如下:

copy(istream_iterator<int, char>(cin), istream_iterator<int, char>(), dice.begin());

它表明第一个创建一个以cin为参数的构造函数,当然它也是个迭代器,与ostream相同,它的第一个参数指出要读取的数据类型,第二个参数指出输入流使用的字符类型。

 

    2、其他有用的迭代器

头文件iterator还提供了其他专用的预定义迭代器类型:

1、reverse_iterator;

2、back_insert_iterator;

3、front_insert_iterator;

4、insert_iterator;

 

       1、reverse_iterator:反向迭代器

对其执行递增操作会导致它被递减,vector类有一个名为rbegin()和rend()的成员函数,分别返回一个指向超尾和第一个元素的反向迭代器,因此可通过以下代码进行反向遍历并输出到屏幕:

ostream_iterator<int, char> out_iter(cout, " ");
copy(dice.rbegin(), dice.rend(), out_iter);

注:显然rbegin()和end()返回相同的超尾,不过二者的类型不同,前者为reverse_iterator,而后者为iterator,rend()和begin()亦然。

然而,问题来了:若rp是被初始化为dice.rbegin()的反转指针,那么由于rbegin()返回超尾,因此不能对rp解除引用。同时,若rend()是第一个元素的位置,则copy()也必须向前推一个位置停止(我愿称之为 超前)。

反向指针通过先递减再解除引用从而解决这两个问题:若rp实际指向位置6,则*rp返回的是位于位置5的元素。

 

    2、3、4三种迭代器将提高STL算法的通用性:copy()或许址能进行复制操作,然而却无法进行添加到另一个容器的前后部等元素。back_insert_iterator将元素插入到容器尾部,front_insert_iterator将元素插入到容器的前端,insert_iterator将元素插入到insert_iterator构造函数指定的地方。

    其中back_insert_iterator只能用于允许在尾部进行quicksort的容器——vector满足。

           front_insert_iterator只能用于允许在起始位置做时间固定插入的容器类型,vector不能满足,而queue满足。

       而insert_iterator则没有限制,因此它可以插入到任何地方,然而若插入到容器前,则对于支持front_insert_iterator的容器类来说,没有后者快。

这些迭代器将容器类型作为模板参数,将实际的容器标识符作为搞糟函数的参数,使用如下:

back_insert_interator<vector<int> > back_iter(dice);

的含义为:为名为dice的vector<int>容器创建一个back_insert_iterator。

insert_iterator的使用如下:不仅它需要一个容器对象,它还需要一个指示插入位置的迭代器参数。

insert_iterator<vector<int>> insert_iter(dice, dice.begin());

5、容器种类

  1、容器概念

   没有与基本容器概念相对应的类型,但概念描述了所有容器类都通用的元素——它指定了所有STL容器类都必须满足的一系列要求。

   容器是存储其他对象的对象,它对这些对象的要求有:

   1、被存储的对象类型必须是同一类型的。

   2、该对象必须是可复制构造、可赋值的。

 同时,所有的容器都提供某些特征和操作。

2、序列

  可以通过添加要求来改进基本的容器概念,如序列(sequence),它保证迭代器至少是正向的要求,以保证元素将按特定顺序排列。(这里面介绍的很多类型已经见过,因此仅对部分进行讲解)
    1、list 模板类:表示双向链表——即可双向遍历,它不支持数组表示法和随机访问,从容器中插入或删除元素后,迭代器指向的地址依然不变。

    2、list 工具箱:由 list 方法所组成,包括 merge、remove、sort、splice(将链表插入到某个pos的前面)、unique(压缩相同的元素为单独的元素)。

其中splice方法:

void splice(iterator pos, list<T, Alloc>)x;

   3、forward_list(C++11)单向队列:它只需要正向迭代器。

   4、priority_queue(优先队列):在底层代码实现为vector而非queue,尽管它满足的是queue的功能,在该队列中,最大的元素被放在首位。(Q:优先队列不是在堆中所学的嘛,为啥用vector实现)

 

3、关联容器(associative container):它将值与键连接到一起。

 其中,值代表:个人的信息,(ps:作者觉得类似于算法导论中提到的卫星数据);键代表唯一的编号。

同时,X::key_type指出键的类型。关联容器的优点在于:允许插入新的元素,以便快速查找信息。

在STL中,提供了4种关联容器:set、multiset、map、和multimap。其中,前两者在set定义,而后二者在map定义。

1、set:关联集合,可反转、排序,值类型与键相同,同时允许只有一个值。

模板的使用:通过模板参数,第一个指定要存储的数据类型,第二个指定排序的模板方法,默认为less<>:

set<string, less<string> > A;

2、multiset:其大致与set无异,不过键和值的类型可以不同,使用如下,

multimap<int, string> codes;

注:以上代码省略了第三个参数即默认参数less<>。

pair<class T, class U>就是该容器概念的一个实现(不要忘记了,关联容器也是容器的概念而非基类or else),实际的值的类型将键数据和数据类型结合为一对。(新的对象)

pair<const int, string> item(213, "Los Angeles");
codes.insert(item);

其中, codes对象的类型即pair……。

其有几个成员函数:

1、count:指出具有该键的成员数目;

2、lower_bound()和upper_bound():将键作为参数,具体的使用见https://blog.csdn.net/qq_40160605/article/details/80150252,这位大佬已经写的很详细了。

3、equal_range():返回两个迭代器,其匹配该键的区间。为获得这两个迭代器,我们也需要使用pair对象进行获取。

 

4、无序关联容器(C++11)

同关联容器一样,它也将值与键关联起来,并用键来查找值。底层差距在于其是使用哈希表实现的,是为了提高添加和删除元素的速度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值