甚麽是 STL
STL 是标准样版程式库(Standard Template Library)的缩写,在经过前篇对样版函式与样版之后,你应该可以理解到 STL 其实就是资料结构与演算法的 template 版本,适用于各种资料型别,STL 当中的样版类别也称为 STL 容器(containers),包括阵列(array)、堆叠(stack)、串列(linked list)...等等,STL 当中的样版函式也称为演算法(algorithms),包括寻找、取代、排序...等等,它们都宣告在 std 命名空间当中,请见下面的例子,vector 样板类别是 STL 当中的阵列类别。
#include <vector>
#include <stdio.h>
using namespace std;
void main(void)
{
// 以 float 型别具现化 STL 的 vector 样板类别:
vector<float> grades;
// 以 vector 的 push_back( ) 把数值 add 到 grades 中,自动增长记忆体空间:
grades.push_back(78.0f); grades.push_back(44.0f);
grades.push_back(89.0f); grades.push_back(55.0f);
grades.push_back(45.0f); grades.push_back(75.0f);
grades.push_back(94.0f); grades.push_back(74.0f);
grades.push_back(83.0f); grades.push_back(65.0f);
// 以 iterator 巡览整个 vector,这是 STL 的标准用法:
float maxGrade = 0.0f;
vector<float>::iterator iter;
for (iter = grades.begin(); iter != grades.end(); ++iter)
{ if (*iter > maxGrade) { maxGrade = *iter; } }
printf("Best grade is %f\n", maxGrade);
}
在这个例子当中,vector<float> 这个类别乃是以 float 型别具现化(realize) vector 这个样板类别,并且利用 push_back( ) 函式(类似前篇例子中的 add( ))来增加资料,push_back( ) 同样会在内部记忆体不足的时候自动增长记忆体空间,其实 vector 和前篇的 ValueArray 样板类别非常相似,到目前为止的,你应该都还感到熟悉。
在 vector 样板类别当中比较令你陌生的,应该就是接下来的 iterator 迴圈了,iter 又可以 ++ 又可以 * 运算子取数值,那麽 iter 到底是个整数变数还是指标呢?概念上来说都不是,在 STL 的容器类别当中另外定义了 iterator 类别来代替 for (int i=0; i<size; ++i) 之类的 i 变数,虽然 iterator 不是个整数,但也覆写(override)它的运算子,使它的行为看起来像整数,又能像指标的 * 运算子一般地取得容器内的资料,这麽作的目的在于希望提供使用者一套他们熟悉的方法,能在不触碰容器类别内部资料成员的前提下(不须传出内部记忆体位址),存取容器内的资料。
在本篇不会详细地逐个介绍 STL 的容器类别,本篇的宗旨仅在于建立你对 STL 容器类别的概念,接下来以 vector 为例说明之。
STL Vector
STL 当中的 vector 样板类别如同前篇的 ValueArray 样板类别一般,是会自动增长空间的阵列类别,这可能是被使用得最广泛的 STL 容器类别,而叠代子(iterator) 则是用来纪录巡览位置的资料结构。如上例一般,vector <typename T> 类别以 push_back( ) 从尾端增加资料,并且提供了 [ ] 运算子取得任意索引的数值,以 size( )指示元素个数。同时也提供了 begin( ) 函式取得开始巡览的叠代子,以 end( )判断是否结束,换言之,上例中巡览整个阵列内容的的迴圈,可以叠代子(iterator)的方式撰写如下:
// 以 iterator 巡览整个 vector,这是 STL 的标准用法:
float maxGrade = 0.0f;
vector<float>::iterator iter;
for (iter = grades.begin(); iter != grades.end(); ++iter)
{ if (*iter > maxGrade) { maxGrade = *iter; } }
printf("Best grade is %f\n", maxGrade);
也可以透过 [ ] 运算子和 size( ) 撰写如下,这是你所熟悉的方式:
// 以整数变数 i 巡览整个 vector,这是 vector 的标准用法:
float maxGrade = 0.0f;
for (int i=0; i<grades.size(); ++i)
{ if (grades[i] > maxGrade) { maxGrade = grades[i]; } }
printf("Best grade is %f\n", maxGrade);
既然如此,为甚麽要有 iterator 这种东西呢?通通都用 i 不就好了吗?这是因为以 vector 来说,由于它内部储存资料的方式同样是连续的记忆体空间,所以它可以提供 [ ] 运算子,但是例如 STL 串列(list)类别以及其他的一些 STL 容器类别,它们虽然不是用连续空间来储存资料所以没有 [ ] 运算子,但它们同样地各自提供了 iterator 类别,同样可以前者的 iterator 迴圈巡览容器内所有的资料。
因此 iterator 的设计可说是为各种不同资料结构的 STL 容器类别,提供了一套共同的资料存取方式,各种容器类别的叠代子(iterator) 均支援 ++ 运算子来移动位置,并可以 * 运算子取回目前位置的数值。
使用 vector 样板类别必须 #include <vector> 并且援用 std 命名空间,它提供了以下的函式:
push_back(T &)
从尾端增加资料。
pop_back( )
从尾端删除资料。
erase(iterator i) 或
erase(iterator i, iterator j)
删除单一或指定范围内的资料。
clear( ) 清除所有的资料。
begin( )
取得从头开始巡览的iterator。
end( )
固定是个无效的iterator数值。
size( )
得知目前的元素数量。
at(i) 或 [i]
取得位于 i 索引位置的资料。
STL 的容器类别
以上介绍了最简单也最常用的 vector 类别,STL 所有的容器类别可分为三类:
序列容器类 (sequence)
vector: 阵列
- 连续空间。
list: 串列
- 非连续空间,无[ ]运算子。
- 插入/删除有效率。
deque:
- 双向 queue。
特性容器类 (adapters)
stack: 堆叠
- 先进后出。
queue: 储列
- 先进先出。
priority_queue: 优先权储列。
关联式容器类 (associative)
map: 序对
- (key, value) 的集合。
- key 不可重複。
multimap
- key 可重複的 map。
- 比较类似 database。
set: 集合
- 数值不可重複。
- 自动排序。
multiset
- 数值可重複的 set。
- 自动排序。
以上这些容器类别的特性,就如同它们所採用的资料结构一般,至于详细的函式与使用方法,你可以到需要使用到的时候再查阅MSDN或其他STL相关书籍,除此之外,如前所述地,除了特性容器类 stack, queue, priority_queue必须要透过特定函式存取资料,以维持先进后出之类的容器特性以外,其他容器均有以下四种叠代子(iterator)。
iterator : 顺向从 begin( ) 到 end( ) 为止。
reverse_iterator : 逆向从rbegin( )到rend( )为止。
这种 iterator 的 ++ 会由后向前走。
const_iterator: 顺向,* 取出的数值是唯读的。
const_reverse_iterator: 逆向,* 取出数值是唯读的
这四种叠代子(iterator)都有 ++, ==, * 三种运算子。
STL Map
必须注意的是各容器类别所提供的叠代子(iterator)乃是它们各自定义的资料结构,并不是共通的,只不过它们都提供了++, ==, * 三种运算子。这是当然的,毕竟没有一种 iterator 资料结构可以用来走访所有种类的容器类别,在此以 STL 另一种常用的容器类别 map 来举例。
map 是一种可以 key 来查询 value 的容器,所以内部都是 (key, value) 的序对,在程式当中可以当作简单的资料表(data sheet)或速查表(look-up table)来使用,在这个例子当中,你可以看到 map 的 [ ] 运算子和 iterator 和 vector 截然不同,map 没有 push_back( ) 函式,反而是覆写了 [ ] 运算子来作为加入(或替换)资料项目的方法,此外 map 的 iterator 有 first 和 second 两个栏位,这也是 vector 的 iterator 没有的。
#include <map>
#include <stdio.h>
using namespace std;
void main(void)
{
// 以 int (key) 与 float (value) 型别具现化 STL 的 map 样板类别:
map<int, float> grades;
// map 没有 push_back( ),它藉由覆写 [] 运算子来新增资料,[] 内的数值是 key 值:
grades[11] = 78.0f; grades[12] = 44.0f; grades[13] = 89.0f; grades[14] = 55.0f;
grades[15] = 45.0f; grades[16] = 75.0f; grades[17] = 94.0f; grades[18] = 74.0f;
grades[19] = 83.0f; grades[20] = 65.0f;
// 以 iterator 巡览整个 map:
map<int, float>::iterator iter;
int bestStudentID = 0;
float maxGrade = 0.0f;
for (iter = grades.begin(); iter != grades.end(); ++iter)
{ if ((*iter).second > maxGrade)
{ bestStudentID = (*iter).first; // 这是 key 值
maxGrade = (*iter).second; } // 这是 value 值
}
printf("Best student is No. %d whose grade is %f\n", bestStudentID, maxGrade);
}
map 的优点在以 key 作查询很有效率,查询方法只要简单地使用 [ ] 运算子就可以了,例如:
// 查询学号为 15 的学生的成绩:
printf("Grade of Student No. %d is %f", 15, grades[15]);
使用 STL 的程式写法是不是很优美呢?但优美背后的代价就是必须熟知运算子覆写与编译器原理,否则有不明错误或隐藏 bug,还有必须熟知资料结构原理,否则用错容器类别会严重伤害程式效率,所以许多人都说 STL 是高手的工艺品。
本篇重点回顾
您知道了吗
STL是把资料结构与演算法给样板化了。
STL提供10种容器,可分为三类,各适用于不同存取需求。
STL的容器提供 iterator 来巡览内容并且存取数值。
STL的 iterator 提供 ++, ==, * 三种运算子。