C++-标准模板库
C++较之C语言强大的功能之一是,C++编译器自带了大量的可复用代码库,我们称为标准模板库(standard template library),STL。标准模板库是一套常用的数据结构的集合,包括链表和一些基于二叉树的数据结构。这些数据结构允许你在创建时指定它们的数据类型,所以可以使用它们来存储任何类型的数据——整型、字符串、或结构体等都可以。
因为这种灵活性,在很多情况下我们可以不用为了完成基本的编程需求构建自己的数据结构,而是用标准模板库来代替。STL可以在几个重要方面提高你的代码层次:
1.你可以开始从需要的数据结构角度来思考问题,而不必担心自己想要的数据结构能否实现;
2.你可以随时使用这些顶级的数据结构,对大多数问题而言,其性能都非常好,所占空间也很少;
3.你不用担心所使用的数据结构进行内存分配和释放等操作的细节。
不过,使用标准模板库也有一些代价:
1.你需要了解标准模板库的各种接口,并学习如何使用它们;
2.错误使用标准模板库所造成的编译错误理解起来不是很容易;
3.并不是每一个你想要的数据结构标准模板库中都有。
标准模板库是一个很大的话题——有些书专讲STL1,所以我的讲述不可能面面俱到。本章的目的是向你介绍一下标准模板库中最常用的数据结构。在这以后,我会在适当的时候使用这些数据结构。
vector,大小可变的数组
在STL中有一个称为vector的数据结构,可以用来代替数组。vector跟数组非常相似,只不过vector的大小可以自动调整,不需要编程人员关心内存分配和已存在元素的移动等细节问题。
使用vector的语法和使用数组的语法不一样。以下是声明一个数组和声明一个vector的对比:
int an_array[ 10 ];
与:
#include <vector> using namespace std; vector<int> a_vector( 10 );
首先,你需要包含(include)头文件vector,以便能随时使用vector数据结构。你还需要使用命名空间(namespace)std,因为vector跟cin和cout类似,都是标准库的一部分。
其次,当你声明一个vector时,必须在尖括号中标识出想要在vector中存储的数据类型:
vector<int>
这个语法使用了C++的一个特性——模板(故名标准模板库)。vector的实现方式允许它存储任何类型的数据,只要告诉编译器,该vector将存储哪种类型的数据即可。换句话说,这里实际上涉及两种类型:一种是所使用的数据结构的类型,它决定了数据的组织方式,另一种是存储在该数据结构中的数据的类型。模板可以组合不同类型的数据结构与存储在该数据结构中的不同的数据类型。
最后,vector的大小放在圆括号中,而不是方括号:
vector<int> a_vector( 10 );
我们使用到这个语法来初始化某个类型的变量。在此例中,我们将值10传给一个初始化例程,称为构造函数,该构造函数将构建一个大小为10的vector。接下来的几章中,我们将了解更多关于构造函数和对象的知识。
一旦构建好了自己的vector,你便能用和访问数组的同样方式来访问vector中的每个元素了:
for ( int i = 0; i < 10; i++ ) { a_vector[ i ] = 0; an_array[ i ] = 0; }
vector的方法调用
vector中提供的功能比数组要多得多。你可以做诸如在vector的末尾添加新元素这样的事情,vector提供了执行这些操作的函数。使用这些函数的语法和你以前所见过的不同。vector利用了C++的一个特性,叫做方法(method),它是一个随着变量类型(在此例中,这个变量类型为vector)一起声明的函数。调用一个方法要使用新的语法,如下:
a_vector.size();
这段代码调用了a_vector的方法size,返回该vector的大小。这有点像访问一个结构体的域,所不同的是,你访问的不是域,而是该结构的方法。尽管size方法显然要对a_vector做一些操作,但你并不需要将a_vector作为一个参数传递给size方法。方法的语法知道要将a_vector作为一个隐含的参数传给size方法。
你可以看做这样的语法:
<variable>.<function call>( <args> );
就好像调用一个属于variable类型的函数一样。换句话说,它有点像写成这样:
<function call>( <variable>, <args> );
本例中,
a_vector.size();
就像是:
size( a_vector );
接下来的几章会继续介绍方法,以及如何声明和使用它们。现在你只需要知道,在vector中有很多方法可以调用,并且调用它们需要使用特殊的语法。这个特殊的语法是进行这种函数调用的唯一方式——你不能写成size(a_vector)。
vector的其他功能
vector还有哪些强大的功能呢?vector可以很容易地增加它所存储的值的数目,无需做任何烦琐的内存分配操作。例如,你若想添加更多的元素到vector中,可以这样写:
a_vector.push_back( 10 );
这个语句增加一个新元素到vector中。具体来说,它指的是,“添加元素10到当前vector的末尾”。vector本身会处理所有的调整大小操作。要是在数组中做这件事,你就必须分配新内存,将所有的值复制过去,最后再添加上你的新元素。当然,vector内部也要分配内存和复制元素,但它会选择一种聪明的大小分配方式,使得如果你不断地添加新元素的话,vector不会每次都重新调整内存大小。
maps
初步介绍了一下map的概念——根据一个值来找到另一个值。这种例子在编程中随处可见:实现一个可以按名称查找邮箱地址的电子邮件地址簿,通过账号查找账户信息,或是允许用户登录游戏,等等。
STL提供了非常方便的map类型,允许指定键(key)和值(value)的类型。例如,一个用来保存简单的电子邮件地址簿的数据结构,类似于你在上一章练习中做过的,可以这样来实现:
#include <map> #include <string> using namespace std; map<string, string> name_to_email;
这里,我们需要告诉map数据结构两个不同的类型:第一个类型string,指的是键的类型;第二个类型也是string,指的是值的类型,本例中指邮箱地址。STL的map有一个很大的特点是,你可以使用跟数组相同的语法,来真正的使用map。
添加一个值到map中的语法跟数组一样,所不同的是,map的键除数字外还可以是其他类型:
name_to_email[ "Alex Allain" ] = "webmaster@cprogramming.com";
访问map中的值的语法几乎完全一样:
cout << name_to_email[ "Alex Allain" ];
真是太方便了!跟使用数组一样简单,却可以存储任何类型的数据。更妙的是,与vector不同,你甚至不需要在使用[]操作符来添加元素之前,先设置map的大小。
你还可以很轻松地从map中删除元素。
如果不想再给我发邮件了,就可以用erase方法把我从你的地址簿中删除:
name_to_email.erase( “Alex Allain” );
你也可以用size方法来查看map的大小:
name_to_address.size();
还可以用empty方法来检查一个map是否为空:
if ( name_to_address.empty() ) { cout << "You have an empty address book. Don't you wish you hadn't deleted Alex?"; }
使用clear方法可以将map真正清除,这太直观了,你肯定不会弄错:
name_to_address.clear();
顺便说一下,STL容器使用一致的命名约定,因此你也可以在vector上使用clear、empty和size方法,跟在map上使用的方式一样。
迭代器(Iterators)
除了存储数据和访问单个元素,有时你可能只是希望遍历某个特定的数据结构中的每个元素。对于数组或vector容器,你可以利用数组的长度来读取每个单独的元素。但是,对于map容器,该怎么办呢?由于map里的键常常不是数字,所以我们不能总是通过一个计数器变量来遍历map中的所有键值。
STL有一个称为迭代器(iterators)的变量专门解决上述问题。迭代器允许你顺次访问任何给定的数据结构中的每个元素,即使该数据结构并未提供做这件事的简单方法。我们先来看看怎样使用一个vector的迭代器,然后再学习如何使用一个迭代器来访问map的元素。迭代器的基本思想是:迭代器变量中存储了数据结构的某个元素的位置,使得你能够访问该位置上的元素。通过调用迭代器提供的方法,可以继续访问数据结构中的下一个元素。
声明一个整型vector的迭代器需要用到特殊的语法,示例如下:
vector<int>::iterator
这个语法大意是说:现在有了一个整型的vector(vector<int>),我们还希望拥有一个能处理它的迭代器,因此用::iterator来表示。那么,迭代器要如何使用呢?由于迭代器中存储着数据结构的某个元素的位置,可以像这样来请求该数据结构的一个迭代器:
vector<int> vec; vec.push_back( 1 ); vec.push_back( 2 ); vector<int>::iterator itr = vec.begin();
调用begin方法将返回一个迭代器,通过它能访问到vector的第一个元素。实际上,可以把迭代器看做一个指针——你可以通过它得到数据结构的某个元素的位置,亦可以使用它来访问该元素。回到刚才的例子,我们可以使用如下语法来访问vector的第一个元素:
cout << *itr; // 输出vector的第一个元素
这里对*运算符的使用,仿佛是在使用指针似的。 这真是太棒了:迭代器跟指针一样,都是位置存储的一种方式。
要获得vector的下一个元素,只需要增加你的迭代器即可:
itr++;
这相当于命令迭代器前往vector的下一个元素。
也可以使用前缀运算符:
++itr;
通过对比当前的迭代器和末端迭代器,我们可以检查是否已经到达迭代遍历的结尾。调用迭代器的end方法可以获得末端迭代器:
vec.end();
因此,循环遍历整个vector的代码可以这样写:
for ( vector<int>::iterator itr = vec.begin(); itr != vec.end(); ++itr ) { cout << *itr << endl; }
这段代码表示:创建一个迭代器,并获得整型vector的第一个元素;当前迭代器不等于末端迭代器时,继续对vector的迭代。输出每个元素。
我们要对这个循环做几个小小的改进。应该避免每次循环时都调用一次vec.end():
vector<int>::iterator end = vec.end(); for ( vector<int>::iterator itr = vec.begin(); itr != end; ++itr ) { cout << *itr << endl; }
实际上,可以将多个变量放到循环的第一个部分中,使代码看起来更整洁些:
for ( vector<int>::iterator itr = vec.begin(), end = vec.end(); itr != end; ++itr ) { cout << *itr << endl; }
我们可以用非常相似的方法来遍历一个map。不过,map的一个元素里不仅仅只有一个值,而是两个:键和值。这样的话,该怎样使用map的迭代器呢?当你间接引用map的迭代器时,它有两个域:first和second。域first为键,而second为对应的值。
int key = itr->first; // 从迭代器中获得键 int value = itr->second; // 从迭代器中获得值
来看一段代码,它将map中的内容以较强的可读性输出出来:
void displayMap (map<string, string> map_to_print) { for ( map<string, string>::iterator itr = map_to_print.begin(), end = map_to_print.end(); itr != end; ++itr ) { cout << itr->first << " --> " << itr->second << endl; } }
这段代码与遍历vector的代码极其相似,真正唯一的区别是map数据结构的使用和迭代器的first和second域的使用。
检查一个值是否在map中
有时候,你会想要检查给定的键是否已经存储在一个map中了。例如,如果你正在通讯簿中查找某人,可能想知道那个人是否真的在通讯簿中。这时,find方法正是你需要的。find方法返回一个迭代器:如果给定的键存在,则返回的是一个持有该键对应的对象位置的迭代器;如果给定的键不存在,返回末端迭代器。
map<string, string>::iterator itr = name_to_email.find( "Alex Allain" ); if ( itr != name_to_email.end() ) { cout << "How nice to see Alex again. His email is: " << itr->second; }
另外,如果你尝试使用普通的方括号运算符访问一个map中不存在的元素:
name_to_email[ "John Doe" ];
那么,map会为你插入这个新的元素,对应的值为空。所以,如果你真的需要知道一个值是否在map中,请使用find方法;除此之外,可以安全地使用方括号运算符。
盘点STL
我们还有很多STL的知识没有讲,但你现在已经掌握了充分利用STL类型的许多基础知识。vector类型是数组的完美替代品。当不需要太在乎插入和修改列表的时间开销时,vector也可以用来取代链表。只有在极少数高级应用,如文件输入输出的情况下,你会选择使用数组而不是vector。
map可能是目前为止最好的一个数据类型了。我经常使用类似map的结构,它使得编写复杂的程序变得更自然,因为你不再需要担心如何创建许多的数据类型。相反,你可以专注于如何解决要解决的问题。在许多方面,map可以取代基本的二叉树——大多数情况下你不用实现自己的二叉树,除非为了特定的性能要求,或者真的需要使用树形结构。这就是STL真正厉害之处——大约80%的情况下,STL提供了核心的数据结构,因此你可以马上动手编写代码,解决特定问题;另外的20%,就是你需要知道如何自己构建数据结构的原因。
在大多数情况下,你不应该自己去实现数据结构——自带的数据结构通常比自己写的要好,速度更快且更完整。但知道如何建立它们会让你更深入地了解如何使用它们,以及如何在确实需要的时候创建自己的数据结构。
cout << name_to_email[ "Alex Allain" ];