C++_标准模板库STL概念介绍1-建立感性认知
标准模板库的英文缩写是STL,即Standard Template Library。
STL里面有什么呢?
它提供了一组表示容器、迭代器、函数对象和算法的模板。
容器是一个与数组类似的单元,可以存储若干值。
STL容器是同质的,即存储的值的类型相同。
算法是完成特定任务的处方(例如对数组进行排序或在链表中查找特定的值)。
迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,属于广义指针。
函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。
STL使得能够构造各种容器(数组、队列、链表)和执行各种操作(搜索、排序和随机排列)。
STL是C++标准的一部分。
STL不是面向对象的编程,而是一种不同的编程模式——泛型编程(generic programming)。
这使得STL在功能和方法上都很有趣。
这篇文章主要通过介绍例子,来对容器、迭代器和算法有感性的认识;领会泛型编程的精神;介绍底层的设计理念,简要地介绍STL。
==================================
一、模板类vector
在计算机中,矢量对应于数组,而不是数学矢量。
在数学中,可以使用N个分量来表示N维数学矢量。
因此从这方面讲,数学矢量类似于一个N维数组。
当然,数学矢量还有一些计算机矢量不具备的特征,例如内乘积和外乘积。
接下来看一下计算矢量有哪些特征:
1、存储了一组可随机访问的值;
2、可以使用索引来直接访问矢量的第n个元素;
3、可以创建一个vector对象,将对象赋给另一个对象,使用[ ]运算符来访问vector元素。
要使类成为通用的,应将它设计为模板类。
STL正是这样做的——在头文件vector中定义了一个vector模板。
要创建vector模板对象,可使用通常的<type>表示法来指出要使用的类型。
另外,vector模板使用动态内存分配,因此可以用初始化参数来指出需要多少矢量:
#include vector
using namespace std;
vector <int> rating(5); //a vector of 5 ints
int n;
cin >> n;
vector<double> scores(n); //a vector of n doubles
由于运算符[ ]被重载,因此创建vector对象后,可以使用通常的数组表示法来访问各个元素:
rating [0] = 9;
for (int i =0; i<n; i++)
cout<< scores[i] <<endl;
分配器:
与string类似,各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。
例如,vector模板的开头与下面类似:
template <class T, class Allocator = allocator<T>>
class vector {...
如果省略该模板参数的值,则容器模板将默认使用allocator<T>类。这个类使用new和delete。
接下来有个示例程序,它使用了这个类。该程序创建了vector对象,一个是int规范、一个是string规范,它们都包含5个元素。
1 // vect1.cpp -- introducing the vector template 2 #include <iostream> 3 #include <string> 4 #include <vector> 5 6 const int NUM = 5; 7 int main() 8 { 9 using std::vector; 10 using std::string; 11 using std::cin; 12 using std::cout; 13 14 vector<int> ratings(NUM); 15 vector<string> titles(NUM); 16 cout<<"You will do exactly as told.Your will enter\n"<<NUM<<" book titles and your ratings (0-10).\n"; 17 int i; 18 for (i=0; i<NUM;i++) 19 { 20 cout << "Enter title #"<<i+1<<": "; 21 getline(cin,titles[i]); 22 cout<<"Enter your rating (0-10):"; 23 cin >>ratings[i]; 24 cin.get(); 25 } 26 cout<<"Thank you. You entered the following:\n"<<"Rating\t\Book\n"; 27 for (i=0; i<NUM; i++) 28 { 29 cout<<ratings[i]<<"\t"<<titles[i]<<endl; 30 } 31 return 0; 32 }
==================================
二、可对矢量执行的操作
除了分配空间外,还可以对模板完成一些基本的操作;
STL容器都提供了一些基本方法,其中包括size()——返回容器中元素数目、swap()——交换两个容器的内容、begin()——返回一个指向容器中第一个元素的迭代器、end()——返回一个表示超过容器尾的迭代器。
那么什么是迭代器呢?它实际上是一个广义指针。它可以是指针,也可以是一个可对其执行类似指针的操作——如解除引用和递增。
通过将指针广义化为迭代器,让STL能够为各种不同的容器类提供统一的接口。
该迭代器的类型是一个名为iterator的typedef,其作用域为整个类。
要为vector的double类型规范声明一个迭代器,可以这样做:
vector<double>::iterator pd; //pd an iterator
假设scores是一个vector<double>类型的对象;
vector<double> scores;
则可以使用迭代器pd执行这样的操作:
pd = scores.begin();
*pd = 22.3;
++pd;
可以看到迭代器的行为像一个指针;
另外C++11还有一个自动类型推断的功能如下:
auto pd = scores.begin();
接下来讨论什么是超过结尾(past-to-end)?它是一种迭代器,指向容器最后一个元素后面的一个元素。
这与C风格字符串的最后一个字符空字符类似,空字符是一个值。
而“超过结尾”是一个指向元素的指针(迭代器)。
end()成员函数标识超过结尾的位置。
如果将迭代器设置为容器的第一个元素,并不断地递增,最终它将到达容器的末尾,从而遍历整个容器的内容。
则可以使用下列代码来显示容器的内容:
for(pd = scores.begin(); pd != scores.end(); pd++)
cout<<*pd<< endl;
另外push-back()是一个方便的方法,它将元素添加到容器的末尾。这样做的时候,它将负责管理内存,增加矢量的长度,使之能容纳新的成员。
可以这样写代码:
vector<double> scores;
double temp;
while(cin>>temp && temp>=0)
socres.push_back(temp);
cout<<"Your entered"<<scores.size()<<"scores.\n";
每次循环都给对象添加元素,无需了解元素的数目,只要有内存,就能添加元素。
earse()方法删除容器中指定区间的元素。它接受两个迭代器参数,这些参数定义了要删除的区间。
了解STL如何使用两个迭代器来定义区间非常重要。
第一个迭代器指向区间的起始位置,第二个迭代器指向位于区间终止处的后一个位置。
代码如下,该代码表示删除begin()和begin()+1指向的元素:
scores.erase(scores.begin(), scores.begin()+2);
我们发现STL文档中会使用[ )方法来表示区间。注意,这种不是C++的标准,仅用于文档表达;
[p1, p2)用来表示p1到p2(不包括p2)的区间。这是一种前闭后开的区间。STL容器就是根据这个约定来定义区间范围的。
[begin(), end()]表示集合的所有内容。
insert()方法的功能和erase()相反。它接受3个迭代器参数,第一个参数指定了新元素的插入位置,第二个和第三个参数定义了要插入的属于另一个对象的新元素区间,该区间通常是另一个容器对象的一部分。
代码如下:
vector<int> old_v;
vector<int> new_v;
...
old_v.insert(old_v.begin(), new_v.begin()+1, new_v.end());
上述代码表示将矢量new_v中的除第一个元素以外的所有元素插入到old_v矢量的第一个元素前面;
另外,对于上面这种情况来说,拥有超尾元素(past-to-end)带来了方便。因为这使得在矢量尾部添加元素非常简单。
old_v.insert(old_v.end(), new_v.begin()+1, new_v.end());
接下来有个示例程序,演示了各个容器方法的使用。
1 //vect2.cpp -- methods and iterators 2 #include <iostream> 3 #include <string> 4 #include <vector> 5 6 struct Review { 7 std::string title; 8 int rating; 9 }; 10 11 bool FillReview(Review & rr); 12 void ShowReivew(const Review & rr); 13 14 int main() 15 { 16 using std::cout; 17 using std::vector; 18 vector<Review> books; 19 Review temp; 20 while (FillReview(temp)) 21 book.push_back(temp); 22 int num = books.size(); 23 if (num >0) 24 { 25 cout<<"Thank you. Your entered the following:\n" 26 <<"Rating\tBook\n"; 27 for (int i =0; i<num; i++) 28 ShowReview(books[i]); 29 cout<<"Reprising:\n"<<"Rating\tBooks\n"; 30 vector<Review>::iterator pr; 31 for (pr = books.begin(); pr != books.end(); pr++) 32 ShowReview(*pr); 33 vector<Review>oldlist(books); 34 if (num >3) 35 { 36 //remove 2 items 37 books.erase(begin.begin()+1, books.begin()+3) 38 cout<< "After erasure:\n"; 39 for (pr = books.begin(); pr != books.end(); pr++) 40 ShowReview(*pr); 41 42 //insert 1 item 43 books.insert(); 44 cout<<"After insertion:\n"; 45 for (pr =books.begin(); pr != books.end(); pr++) 46 ShowReview(*pr); 47 } 48 books.swap(oldlist); 49 cout<<"Swapping oldlist with books:\n"; 50 for(pr = books.begin(); pr != books.end();pr++) 51 ShowReview(*pr); 52 } 53 else 54 cout<<"Nothing entered, nothing gained.\n"; 55 return 0; 56 } 57 58 bool FillReview(Review & rr) 59 { 60 std:;cout<<"Enter book title(quit to quit):"; 61 std::getline(std::cin, rr.title); 62 if (rr.title == "quit") 63 return false; 64 std::cout<<"Enter book rating: "; 65 std::cin >> rr.rating; 66 if(!std::cin) 67 return false; 68 while(std::cin.get() != '\n') 69 continue; 70 return true; 71 } 72 73 void ShowReview() 74 { 75 std::cout<<rr.rating<<"\t"<<rr.title<<std::endl; 76 }
==================================
三、对矢量可执行的其他操作
程序员通常要对数组执行很多操作,如搜索、排序、随机排序等;
矢量模板包含了执行这些常见的操作方法吗?没有!
STL从更广泛的角度定义了非成员函数来执行这些操作,既不是为每个容器类定义find()成员函数,而是定义了一个适用于所有容器类的非成员函数find()。
这种设计理念省去了大量的重复工作。
举个例子,假设有8个容器类,需要支持10种操作。如果每个类都有自己的成员函数,则需要定义80个成员函数。
但是采用STL的方法,只需要定义10个非成员函数即可。
在定义新的容器类时,只要遵循正确的指导思想,则它也可以使用已有的10个非成员函数来执行查找、排序等操作。
当然有时候,即使有执行相同任务的非成员函数,STL有时也会定义一个成员函数。这是因为对有些操作来讲使用类的特定算法的效率比通用算法高。
因此,vector的成员函数swap()的效率比非成员函数swap()高,但非成员函数让您能够交换两个类型不同的容器的内容。
接下来讨论3个具有代表性的STL非成员函数:for_each()、random_shuffle()和sort()。
for_each()函数可用于很多函数类,它接受3个参数。前两个是迭代器用于定义容器中的区间,最后一个是指向函数的指针(更普遍地说,最后一个参数是一个函数对象,函数对象将稍后介绍)。
可以将代码:
vector<Review>::iterator pr;
for (pr = books.begin(); pr != books.end(); pr++)
ShowReview(*pr);
替换为:
for_each(books.begin(),books.end(), ShowReview);
这样可以避免显式地使用迭代器变量。
Random_shuffle()函数接受两个指定区间的迭代器参数,并随机排列该区间中的元素。
例如,下面的语句随机排列books矢量中的所有元素:
random_shuffle(books.begin(), books.end());
sort()函数也要求容器支持随机访问。该函数有两个版本。
第一个版本接受两个定义区间的迭代器参数,并使用为存储在容器中的类型元素定义的<运算符,以对区间中的元素进行操作。
下面的语句按照升序对coolstuff的内容进行排序,排序时使用内置的<运算符对值进行比较:
vector<int> coolstuff;
...
sort(coolstuff.begin(), coolstuff.end());
如果容器元素是用户定义的对象,则要使用sort(),必须定义能够处理该类型对象的operator<()函数。
例如,如果为Review提供了成员函数或非成员函数operator<(),则可以对包含Review对象的矢量进行排序。
由于Review是一个结构,因此其成员是公有的,这样的非成员函数将为:
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;
else
return false;
}
有了这样的函数,就可以对包含Review对象的矢量进行排序了;
sort(books.begin(), books.end()); //这两个参数是指定容器区间的迭代器;
上述的排序是按照升序的方式进行,使用内置的<运算符进行排序;
如果想要按照降序的方式的话,可以自己定义一个函数WorseThan();
有了这个函数后,就可以对包含Review对象的book矢量进行降序排序:
sort(books.begin(), books.end(),WorseThan());
接下来这段程序将演示STL非成员函数的用法:
1 //vect3.cpp -- using STL functions 2 #include <iostream> 3 #include <string> 4 #include <vector> 5 #include <algorithm> 6 7 struct Review { 8 std::string title; 9 int rating; 10 } 11 12 bool operator<(const Review & r1, const Review & r2); 13 bool worsethan(const Review & r1, const Review & r2); 14 bool FillReview(Review & rr); 15 void ShowReview(const Review & rr); 16 17 int main() 18 { 19 using namespace std; 20 21 vector<Review> books; 22 Review temp; 23 while(FillReview(temp)) 24 books.push_back(temp); 25 26 if(books.size()>0) 27 { 28 cout<<""<<books.size()<<""<<""; 29 for_each(books.begin(), books.end(), ShowReview); 30 31 sort(books.begin(), books.end()); 32 cout<<""; 33 for_each(books.begin(), books.end(), ShowReview); 34 35 sort(books.begin(), books.end(),worseThan); 36 cout<<""; 37 for_each(books.begin(), books.end(), ShowReview); 38 39 random_shuffle(books.begin(), books.end()); 40 cout<<""; 41 for_each(books.begin(), books.end(), ShowReview); 42 } 43 else 44 { 45 cout<<"No entries"; 46 } 47 cout<<"Bye.\n"; 48 return 0; 49 } 50 51 bool operator<(const Review & r1, const Review & r2) 52 { 53 if(r1.title < r2.title) 54 return true; 55 else(r1.title == r2.title && r1.rating <r2.rating) 56 return true; 57 else 58 return false; 59 } 60 61 bool worsethan(const Review & r1, const Review & r2) 62 { 63 if(r1.rating < r2.rating) 64 return true; 65 else 66 return false; 67 } 68 69 70 bool FillReview(Review & rr) 71 { 72 std::cout<<"Enter book title(quit to quit)"; 73 std::getline(std::cin, rr.title); 74 if(rr.title == "quit") 75 return false; 76 std::cout<<"Enter book rating:"; 77 std::cin>>rr.rating; 78 if(!std::cin) 79 return false; 80 //get rid of rest of input line 81 while(std::cing.get() != "\n") 82 continue; 83 return true; 84 } 85 86 void ShowReview(const Review & rr) 87 { 88 std::cout<<rr.rating<<"\t"<<rr.title<<std::endl; 89 }
==================================
四、基于范围的for循环(C++11)
接下来介绍一种新型的for循环方式,叫做基于范围的for循环;
这种for循环其实是为用于STL而设计的;
下面有个示例:
double price[5] = {4.99, 10.99, 6.87, 7.99, 8.49};
for (double x : prices)
cout<<x<<std::endl;
首先在for循环的括号中,声明一个变量,该变量的类型与容器中存储的内容相同。然后指出容器的名称。
接下来,循环体使用指定的变量依次访问容器的每个元素。
由此我们可以改写代码:
将代码:for_each(books.begin(), books.end(), ShowReview);
改写成:for(auto x : books) ShowReview(x);
注意这里修饰x变量的关键字是auto。自动变量,这是因为根据books的类型vector<Review>,编译器可以推断出x的类型为Review,而循环将依次将books中的每个Review对象传递给ShowReview()。
这个是自动类型推断功能。
基于范围的for循环还可以修改容器的内容,诀窍是使用一个引用参数。
void InflateReview(Review &r){r.rating++}
接下来:
for(auto x:books) InflateReview(x);