软件编程|STL使用总结

1.概述 泛型编程思想最早缘于A.Stepanov提出的部分算法可独立于数据结构的论断。20世纪90年代初A.Stepanov和Meng Lee根据泛型编程的理论用C++共同编写了STL。但直至1998年,STL才成为C++的正式标准。在后来的几年中,各大主流编译器也都相继加入了对STL的支持,至此STL才开始得到广泛的应用。 STL体现的是泛型编程的核心思想:独立数据结构和算法(这是一种独立于OO的编程哲学)。 STL主要由几个核心部件组成,即迭代器、容器、算法、函数对象、适配器。容器即物之所属;算法是解决问题的方式;迭代器是对容器的访问逻辑的抽象,是连接算法和容器的纽带;迭代器通过添加了一种间接层的方式实现了容器和算法之间的独立;函数对象,就是重载了operator()操作符的对象;适配器是通过组合特定的容器实现的一种新的数据结构。在后续的内容中,我们将对几个核心部件的基础应用进行详细的描述。 2.基础 C++产生的历史背景赋予了C++太多的职责,比如兼容C、综合的编程语言等,这些虽然赋予了C++强大的功能,但同时也扔给了极大的复杂度。在这篇文章中,我们并不打算将你带入C++的复杂地带,但是需要你有一定的C++基础,比如类、结构等。 STL深深地植根于C++的基础设施,这其中包括了内联、函数对象、函数模板、类模板等。 2.1. 内联 内联是C++中一种特殊的语言机制,使用inline来标识。C++在编译inline标识的函数时,将根据特定的规则将inline函数的代码直接插入到inline函数的调用位置,以此来提高程序的运行效率,但同时也在一定程度上导致了代码的膨胀。请看下面的例子: inline全局函数 inline void max{…}; inline成员函数 class A{ public: inline void max{…}; } 2.2. 函数对象 函数对象,就是重载了operator()操作符的对象。相对函数指针,函数对象可以具有状态,更安全,更灵活,基本没有额外的开销。在STL中,函数对象经常被用作算法的输入参数或容器的实例化的参数。请看下面的例子: 定义函数对象类 classs LessThan{ public: LessThan(int val): m_val(val){} bool operator()(int val){return m_val < val; } private: int m_val; }; 定义函数对象 LessThan less(5); 调用定义函数对象 less(10); //返回为true 2.3. 函数模板 C++中的模板是STL实现的技术基础。C++中的模板可分为函数模板和类模板。函数模板抽象了针对不同类型的同一性质的操作。如针对 int/long/string的max操作。请看下面的例子: 定义求取两个类型的值或对象的最大值的操作 template T max(T a,T b){return a>b ? a : b; } 求取两个int值的最大值 max(0,10); //返回10 求取两个string对象的最大值 max(string(“Hello”),string(“world”)); //返回string(“world”) 2.4. 类模板 C++中的模板是STL实现的技术基础。C++中的模板可分为函数模板和类模板。类模板抽象了针对不同类型的同一类事务。如针对 int/long/string的Stack。请看下面的例子: 定义一个通用堆栈Stack template class Stack{ public: inline void push(const T& value){…} T pop(){…} void clear(){…} bool empty() const {…} }; 声明一个int类型的Stack typdef Stack IntStack; 声明一个string类型的Stack typdef Stack IntStack; 3.迭代器 STL中的迭代器是C++指针的泛化,它在算法和容器之间充当一个中间层,为处理不同的数据结构提供了一致的方式。迭代器也可以看作是对容器数据结构访问的一种约束。STL中的迭代器可分为类:随机存取迭代器(random-access-iterator),双向存取迭代器(bidirectional-access-iterator),前向迭代器(forward iterator),输入迭代器(input-iterator),输出迭代器(output-iterator)。它们之间的继承关系如下图: input-iteratoroutput-iteartorforward-iterator:output-iteartor , input-iteratorbidirectional-access-iterator : forward-iterator random-access-iterator:bidirectional-access-iterator 图一 迭代器关系图 3.1. 输入迭代器 输入迭代器也可以称之为前向的只读访问器,首先它提供了对容器的只读访问,其次它只能在容器中进行前向迭代(即只提供++操作)。所有的容器的迭代器都具有输入迭代器的特征。通过输入迭代器你可以进行下面三种操作: 1.V = *X++ 2.V = *X,X++ 3.V = *X,++X 注:V为值,X为迭代器 3.2. 输出迭代器 输出迭代器也可以称之为前向的只写访问器,首先它提供了对容器的只写访问,其次它只能在容器中进行前向迭代(即只提供++操作)。通过输出迭代器你可以进行下面三种操作: 1.*X++ = V 2.*X = V, X++ 3.*X = V, ++X 注:V为值,X为迭代器 3.3. 前向迭代器 前向迭代器继承自输入和输出迭代器,因此具有输入和输出迭代器的所有特征,也即提供对容器数据结构的读写访问但也只具有前向迭代的能力(即只提供++操作)。因此,你可以对前向迭代器进行操作:R == S /++R == ++S。 请看下面的例子: 定义一个利用前向迭代器进行线性查找的算法 template ForwardIterator linear_search(ForwardIterator first,ForwardIterator last,const T& value) { for (; first != last; ++first){ if (*first == value) return first; } return last; } 测试 int ia[] = {35,3,23}; vector vec(ia,ia+3); vector::iterator it = linear_search(vec.begin(),vec.end(),100); if (it != vec.end()) std::cout << "Found" << endl; else std::cout << "Not Found" << endl; 测试结果为:Not Found 3.4. 双向存取迭代器 双向存取迭代器从前向迭代器继承过来,因而具有前向迭代器的所有特征,双向存取迭代器还具有后向访问能力(即只提供--操作)。请看下面的例子: 定义一个利用双向迭代器排序算法 template void sort_me(BidirectionalIterator first,BidirectionalIterator last,Compare comp) { for(BidirectionalIterator i = first; i != last; ++i) { BidirectionalIterator _last = last; while(i != _last--) { if (comp(*i,*_last)) iter_swap(i,_last); } } } 测试 int ia[] = {123,343,12,100,343,5,5}; vector vec(ia,ia+7); sort_me(vec.begin(),vec.end(),less()); copy(vec.begin(),vec.end(),ostream_iterator(cout," ")); std::cout << endl; 测试结果为:5 5 12 100 123 343 343 3.5. 随机存取迭代器 随机存取迭代器从双向存取迭代器继承过来,因而具有双向存取迭代器的所有特征。所不同的是,利用随机存取迭代器你可以对容器数据结构进行随机访问,因而随机存取迭代器还可以定义下面的操作: 1.operator+(int) 2.operator+=(int) 3.operator-(int) 4.operator-=(int) 5.operator[](int) 6.operator-(random-access-iterator) 7.operator>(random-access-iterator) 8.operator<(random-access-iterator) 9.operator>=(random-access-iterator) 10.operator<=(random-access-iterator) 在STL中,随机存取双向迭代器只能作用于顺序容器。请看下面的例子: 测试输出vector里面的数据 int ia[] = {123,343,12,100,343,5,5}; vector vec(ia,ia+7); for(int i = 0; i < vec.size(); ++i) std::cout << vec[i] << " "; 测试结果为:123 343 12 100 343 5 5 4.容器 容器即物之所在。容器是STL的核心部件之一,是迭代器的依附,是算法作用的目标。 STL中的容器可分为顺序容器(Sequence Container)和关联容器(Associative Container)。容器适配器(Container Adaptor)是对顺序容器(Sequence Container)或关联容器(Associative Container)进行包装而得到的一种具有更多约束力(或功能更强大)的容器。 下表列出的是STL中的主要(标准和非标准的)容器:

顺序容器 (Sequence Container) 容器 备注
vector
stack 非STL标准, vector的适配器(Adaptor)
list
slist 非STL标准, list的适配器(Adaptor)
deque
priority_queue 非STL标准, deque的适配器(Adaptor)
queue 非STL标准, deque的适配器(Adaptor)
关联容器 (Associative Container) set 底层数据结构是RB-tree(红黑树)
multiset 底层数据结构是RB-tree(红黑树)
map 底层数据结构是RB-tree(红黑树)
multimap 底层数据结构是RB-tree(红黑树)
hashtable 非STL标准
hash_set 非STL标准,底层数据结构是hashtable
hash_map 非STL标准,底层数据结构是hashtable
hash_multiset 非STL标准,底层数据结构是hashtable
hash_multimap 非STL标准,底层数据结构是hashtable)
4.1. 共性 所有的容器都是物之所在,这就决定了它们必然存在很多共性,这些共性包括迭代器、大小等属性。容器与容器之间的主要区别体现在对数据的操作上。 每类容器都包含四个迭代器:iterator(正向迭代器)、const_iterator(常正向迭代器)、reverse_iterator(反向迭代器)、const_reverse_iterator(常反向迭代器)。因此你可以按照下面的方式获取每个容器的相应的迭代器: 获取正向迭代器 C::iterator it = c.begin(); C ::iterator it = c.end(); 获取反向迭代器 C ::reverse_iterator it = c.rbegin(); C :: reverse_iterator it = c.rend() 获取常正向迭代器 C::const_iterator it = c.begin(); C :: const_iterator it = c.end(); 获取常反向迭代器 C :: const_ reverse_iterator it = c.rbegin(); C :: const_ reverse_iterator it = c.rend() 注: C 为容器类型, c 为 C 的实例 所有容器是数据的存在之处,可以看作的是数据的集合,因此它们都会有大小、是否为空等属性,因此你可以按照下面的方式获取所有的容器的公共属性: 获取容器的大小 c.size(); 判断容器是否为空 c.empty(); 4.2. 顺序容器 顺序容器中所有的元素在容器中的物理位置都是按照特定的次序进行存放的,区别于关联容器的是顺序容器中的元素的位置都是既定的。被纳入STL标准的顺序容器包括vector、list、dequeue。 序列容器之间的共性除了容器之间应有的共性之外,还有对数据操作的接口(非实现)上: c.push_back c.pop_back c.push_front c.pop_front c.back c.front c.erase c.remove 4.2.1.vector vector和数组具有同样的内存处理方式。不同于数组的是:数组是静态空间,一旦分配了就不能被改变,因而空间的分配非常地不灵活;vector是动态空间,即空间可以被动态分配,因而空间的分配很灵活。可以说vector是相对数组的一种更高级的数据结构。 vector中的迭代器的种类为随机存取迭代器(random-access-iterator)。 vector不同于其它顺序容器(Sequence Container) 的时,它具有capacity属性,可以通过vec.capacity()来获取。 请看下面的例子: 构造vector int ia[] = {123,343,12,100,343,5,5}; vector vec(ia,ia+7); //{ 123,343,12,100,343,5,5} vector vec1(2,4); //{4,4} vector vec2(4); {0,0,0,0} 输出vector中的所有数据 vector::iterator itEnd = vec.end(); for(vector::iterator it = vec.begin(); it != itEnd; ++it) std::cout << *it << endl; 添加数据 vector::iterator it(vec.rbegin().base()); vec.insert(it,1); 或 vector::iterator it = vec.begin(); vec.insert(it,1); 4.2.2.list list是链表的抽象数据结构(ADT)。list中的所有数据在空间分配上不一定是连续存放的。相对vector,list没有capaciy属性。 list中的迭代器的种类为双向存取迭代器(bidirectional -access-iterator)。 请看下面的例子: 构造list int ia[] = {123,343,12,100,343,5,5}; list ls; list ls(ia,ia+7); //{ 123,343,12,100,343,5,5} list ls(2,4); //{4,4} list ls(4); { 0,0,0,0} 输出list中的所有数据 list::iterator itEnd = ls.end(); for(list::iterator it = ls.begin(); it != itEnd; ++it) std::cout << *it << endl; 4.2.3.deque deque,即双向链表。相对vector,deque也是连续空间,但不是vector的连续线性空间。 deque中的迭代器的种类为随机存取迭代器(random-access-iterator)。 deque成员函数如下表:
函数 描述
c.assign(beg,end) c.assign(n,elem) 将[beg; end)区间中的数据赋值给c。 将n个elem的拷贝赋值给c。
c.at(idx) 传回索引idx所指的数据,如果idx越界,抛出out_of_range。
c.back() 传回最后一个数据,不检查这个数据是否存在。
c.begin() 传回迭代器重的可一个数据。
c.clear() 移除容器中所有数据。
deque c deque c1(c2) deque c(n) deque c(n, elem) deque c(beg,end) c.~deque() 创建一个空的deque。 复制一个deque。 创建一个deque,含有n个数据,数据均已缺省构造产生。 创建一个含有n个elem拷贝的deque。 创建一个以[beg; end)区间的deque。 销毁所有数据,释放内存。
c.empty() 判断容器是否为空。
c.end() 指向迭代器中的最后一个数据地址。
c.erase(pos) c.erase(beg,end) 删除pos位置的数据,传回下一个数据的位置。 删除[beg,end)区间的数据,传回下一个数据的位置。
c.front() 传回地一个数据。
c.get_allocator 使用构造函数返回一个拷贝。
c.insert(pos,elem) c.insert(pos,n,elem) c.insert(pos,beg,end) 在pos位置插入一个elem拷贝,传回新数据位置。 在pos位置插入>n个elem数据。无返回值。 在pos位置插入在[beg,end)区间的数据。无返回值。
c.max_size() 返回容器中最大数据的数量。
c.pop_back() 删除最后一个数据。
c.pop_front() 删除头部数据。
c.push_back(elem) 在尾部加入一个数据。
c.push_front(elem) 在头部插入一个数据。
c.rbegin() 传回一个逆向队列的第一个数据。
c.rend() 传回一个逆向队列的最后一个数据的下一个位置。
c.resize(num) 重新指定队列的长度。
c.size() 返回容器中实际数据的个数。
C1.swap(c2) 将c1和c2元素互换。
4.3. 关联容器 关联容器(Associative Container)提供了根据key快速检索数据的能力。在关联容器(Associative Container)中,key和元素都是成对(pair)存在的,你可以调用std::make_pair使用key和元素值来构建一个pair。 STL提供的关联容器包括set、multiset、map、multimap。 set和map只支持唯一键(unique key),即对个key最多只保存一个元素。multiset和multimap则支持多个key,一个key可以对应多个元素。 set和map的区别在于,在set里面key和元素是同一个值,而在map里面key和元素分开存储。 4.3.1.set set是集合的抽象数据结构(ADT)。不同于数学意义上的集合,STL中的set的所有的元素都是有序的而且set中所有的元素都是唯一的。 set中的迭代器的种类为双向存取迭代器(bidirectional-access-iterator)。 请看下面的例子: 构建一个set set fruits; 往set中添加数据 fruits.insert("apple"); fruits.insert("orange"); fruits.insert("banana"); 输出set set::iterator itEnd = fruits.end(); for (set::iterator it=fruits.begin(); it != itEnd; it++) cout << *it << " "; 输出结果 apple banana orange 4.3.2.multiset multiset和set基本相同,所不同的是,一个multiset中的元素是可以重复的。 multiset中的迭代器的种类为双向存取迭代器(bidirectional-access-iterator)。 multiset的操作同set。 请看下面的例子: 构建一个set set fruits; 往set中添加数据 fruits.insert("apple"); fruits.insert("orange"); fruits.insert("apple"); fruits.insert("banana"); fruits.insert("banana"); 输出set set::iterator itEnd = fruits.end(); for (set::iterator it=fruits.begin(); it != itEnd; it++) cout << *it << " "; 输出结果 apple apple banana banana orange 4.3.3.map map是字典的抽象数据结构(ADT)。map中的所有的元素都会根据key自动进行排序,而且所有的元素都是唯一的。map中的所有的元素都是pair,即键(key)和值(value)组成的序列(pair中的第一个元素为key,第二个元素为value)。 map中的迭代器的种类为双向存取迭代器(bidirectional-access-iterator)。 请看下面的例子: 构建一个map typedef std::map EMPLOYEE_MAP; EMPLOYEE_MAP employees; 往map中添加数据 employees.insert(EMPLOYEE_MAP::value_type(25301, "A")); employees.insert(EMPLOYEE_MAP::value_type(25302, "B")); employees.insert(EMPLOYEE_MAP::value_type(25303, "C")); employees.insert(EMPLOYEE_MAP::value_type(25304, "D")); employees.insert(EMPLOYEE_MAP::value_type(25305, "E")); 输出map EMPLOYEE_MAP::iterator itEnd = employees.end(); for (EMPLOYEE_MAP::iterator it = employees.begin(); it != itEnd; ++it) { std::cout << it->first << "-"; std::cout << it->second << endl; } 输出结果 25301-A 25302-B 25303-C 25304-D 25305-E 4.3.4.multimap multimap和map基本相同,所不同的是,一个multimap中的key可以重复。 multimap中的迭代器的种类为双向存取迭代器(bidirectional-access-iterator)。 multimap的操作同set。 4.3.5.其它 这里要提到的就是hashtable,即哈希表的抽象数据结构(ADT)。 因为hashtable不是STL标准的一部分,部分编译器(如Microsoft Visual C++)并没有提供hashtable的实现。 hashtable中的迭代器的种类为前向迭代器(forward-iterator)。 在SGI的STL中,有hashtable的实现,其中的hash_set,hash_map,hash_multiset, hash_multimap都是基于hashtable而构建的,是hashtable的适配器(Adaptor)。而在 Microsoft Visual C++的STL中,并没有提供STL的实现。其中的hash_set,hash_map,hash_multiset,hash_multimap都是基于STL中的hash算法而构建的。 hash_set的操作基本同set; hash_map的操作基本同map; hash_multiset的操作基本同multiset; hash_multimap的操作基本同multimap。 5.算法 STL中的算法以迭代器为参数,通过作用于迭代器而达到处理容器数据的目的。所有的算法都前两个参数都是以对迭代器(iterator),通常称为first和last。事实上,算法所处理的迭代器范围为[first,last),如果first==last则表明此范围为空。按照处理问题的不同,STL算法可分为四组,分别是: 1.不改变序列的算法(None-mutating sequence Algorithm) 2.改变序列的算法(Mutating sequence Algorithm) 3.排序以及相关算法(Sorting and related Algorithm) 4.常用数字算法(Generalized numeric Algorithm) 很多算法都有if版本,如remove_if,这类算法会根据判断条件进行相应的操作;有一些算法具有copy_if或copy版本,不同于非copy_if或copy版本的是,这些操作将获取的值改写另外的迭代器。 本文并不打算对这些算法作详细的介绍,只是对这些算法做简单性的描述。更多的内容,推荐参考《C++ Primer》或《STL和泛型程序设计》。 5.1. 改变序列的算法 这类算法以输入迭代器(input-iterator)为输入参数,但不改写迭代器迭代器范围内的数据。典型的算法有:find、count、equal、search、for_each等。 5.2. 不改变序列的算法 这类算法以输入迭代器(input-iterator)为输入参数,往往还带有一个输出迭代器(output-iterator),对输入迭代器(input-iterator)处理的结果写入输出迭代器。典型的算法有:copy、transform、remove、rotate、reverse、fill、replace等。 5.3. 排序以及相关算法 这类算法往往以双向迭代器(bidirectional-iterator)为输入输出参数,对迭代器范围内的数据进行排序,最终序列里面的值都会发生改变。之所以把排序以及相关算法和”不改变序列的算法”分离出来是因为这类算法解决了是算法领域的一类问题,而且这类问题在算法领域的重要性异常突出。典型的算法有:sort、partial_sort、nth_element、qsort、stable_sort、partition、merge、inplace_merge等。 5.4. 常用数字算法 这类算法往往以双向迭代器(bidirectional-iterator)为输入输出参数,对迭代器范围内的数据进行数字操作(如内积等),最终序列里面的值都会发生改变。这类算法解决的是数学领域相关的问题,因此被划为一类。典型的算法有:next_permutation、pre_permutation、inner_product等。 6.适配器(Adaptor) 适配器(Adaptor)是为了增强功能或/和添加约束,对容器、迭代器、算法重新进行了包装的模板类。设计模式中的Decorate模式的基本思想是实现适配器(Adaptor)的理论/实践基础。在STL中,适配器可分为三类:容器适配器、迭代器适配器以及算法适配器。 6.1. 容器适配器 容器适配器是对特定的容器进行Decorate而形成的新的模板类。常见的容器适配器有: 1.vector适配器 stack:堆栈,是一种FILO的数据结构。 2.deque适配器 priority_queue:优先级队列,队列里面的所有数据都是按照优先级进行排序的。 queue:单向队列。 3.list适配器 slist:单向链表。 4.hashtable的适配器 hash_map,hash_set,hash_multimap,hash_multiset 6.2. 迭代器适配器 迭代器适配器是对特定的迭代器进行Decorate而形成的新的模板类。常见的迭代器适配器有: 1.反向迭代器(reverse iterator) 相对迭代器(iterator),反向迭代器(reverse iterator)是按照相反的顺序对容器的数据进行访问的,如 vector::reverse_iterator ritBegin = vec.rbegin(); vector::reverse_iterator ritEnd = vec.rend(); 2.插入迭代器(insert iterator) 插入迭代器简化了向容器中插入元素的工作,指定了向容器中插入元素的位置。STL中有三种插入迭代器: n 前向插入迭代器 在容器的前面插入元素,如调用容器的push_front n 后向插入迭代器 在容器的后面插入元素,如调用容器的push_back n 任意插入迭代器 在容器的任意位置插入元素,如调用容器的insert 3.原始存储迭代器(raw storage itertor) 允许算法将其结果保存到没有初始化的内存中。 6.3. 函数适配器 函数对象适配器是对某类特定函数对象进行Decorate而形成的新的模板类。常见的迭代器适配器有: 1.否定者 (negator) STL中有两个否定者not1和not2,分别对一元和二元谓词(predicate)的执行结果取反。 2.绑定者(Binder) STL中有两个绑定者binder1st和binder2nd(你可以通过调用函数bind1st和bind2nd分别构建这两个绑定者),分别将特定值绑定到函数对象的第1个和第2个参数s。 3.函数指针函数适配器(Adaptors for pointers to function) 因为STL中的算法一般都使用函数对象作为参数。如果需要使用原始的函数指针用于这些算法的输入参数,那么我们就需要将这些原始指针转化成特定的函数对象。STL提供了这样的转换函数,包括: n 转化成员函数:mem_fun,mem_fun_ref,mem_fun1,mem_fun1_ref n 转化非成员函数:ptr_fun 7.资源 1.STL中文站: http://www.stlchina.org 2. Boost: http://www.boost.org/ 3.SGI STL: http://www.sgi.com/tech/stl/ 4. STL port: http://www.stlport.org/

    推荐阅读