您的位置:首页 > 编程语言 > C语言/C++

C++ STL内部简单细节整理

2015-06-28 21:04 555 查看
对于使用C++语言进行项目开发的同学,STL必然是必须掌握并且熟练的技术。除了能够熟练使用,我们当然也有必要知道其内部实现原理。当然,对于新手或者并属于一线开发者的同学,一下子看懂STL源码是不现实的,但是我们可以从简单的地方入手,慢慢去了解掌握它。下面我就总结一些最基本的细节。

STL的构成

大部分人可能知道STL包括容器,迭代器,算法。其实,STL还包括比较重要的函数对象,适配器,内存分配器,概念和模型。这里分别挑重点说一下。

函数对象(Function Object)

先说函数对象吧,哈哈。在C++中我们知道函数指针,它将具有相同类型和个数的输入参数和返回值的一组函数抽象出来。这为代码灵活性做出来贡献。但是在STL中,我们使用函数对象而不是函数指针——这其实也是C++高级特性中的一贯做法,将指针封装成一个对象去使用。迭代器啦,智能指针啦,都是这样子的。需要注意的一点是,为了使用上直观,我们需要重载函数对象的运算符(),这样当函数调用其()成员函数时,就好像还是在使用一个函数一样。

适配器(Adapter)

C++ STL中所谓的适配器,作用相当于一个类型转换。可以转换函数对象,迭代器,甚至是容器等。其实这是为了利用已有的代码或者数据结构泛化实现扩展的数据结构而已,因为好多STL的模块内部实现是差不多的。比如双参数的函数对象,可以通过赋予其中一个参数某个值,转换成单参数的函数对象;reverse_iterator迭代器是由iterator迭代器转换移动方向相反之后实现的;stack栈容器,本身并没有什么实质性的代码,只是将底层容器转换为“后进先出”的stack容器。

容器(Container)

容器是STL中专门存放数据的一组数据结构。其它的不多说,这里主要说明一下每一种容器都是用什么基本的数据结构实现的,这个在面试中经常遇到。

1 vector

vector可能是最常用的数据结构了,其内部是用数组实现的。也难怪,vector就是一个高级数组,尤其是少有的支持用中括号随机存储。既然是数组实现的,那么当所存储的数据超出初始数组容量大小的时候怎么办?好办,这样的话,vector会重新申请一块更大的内存,大小为原来的两倍,然后将原来内存的数据拷贝到新内存中,再释放原内存。这样带来一个问题,当数据量不断增大,vector就会不断进行动态内存的申请和释放,这样严重影响效率。于是,如果我们开始的时候知道数据的最大数量,就可以在最开始的时候,为vector分配足够大的内存,用的的函数是vector对象的reserve()和resize()函数。vector 的reserve增加了vector的capacity,但是它的size没有改变!而resize改变了vector的capacity同时也增加了它的size!原因如下:

reserve是容器预留空间,但在空间内不真正创建元素对象,所以在没有添加新的对象之前,不能引用容器内的元素。加入新的元素时,要调用push_back()/insert()函数。

resize是改变容器的大小,且在创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。此时再调用push_back()函数,是加在这个新的空间后面的。

两个函数的参数形式也有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,

第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。

2 list

list可以认为就是一个环形双向列表,只不过封装后成为了STL的一个普通容器。

3 deque

deque则采用的是vector和list的结合,所谓结合,并不是两种数据结构的结合,而是某些性能上的结合。我们知道,vector支持随机访问,而list支持常量时间的删除,deque支持的是随机访问以及首尾元素的删除。deque的内部实现嘛,首先盗图一张:



deque容器的元素数据采用一种分块的线性结构存储。deque分成若干线性存储块,称为deque块,块的大小一般为512个字节,元素的数据类型所占用的字节数,决定了每个deque块可容纳的元素个数。所有的deque块使用一个Map块进行管理,每个Map块数据项记录各个deque块的首地址。类似于一个二维数组,要支持随机存储,首先需要从map中找到“二维数组”的第一维,然后再从相应的连续空间中找“二维数组”的第二维。更多详细说明请点击这位老兄的博客。

4 stack和queue

这两个容器合起来叫堆栈,stack是栈,queue是堆。它们是直接利用STL中的现有容器泛化实现的。默认使用双端队列deque的数据结构,当然也可以采用其他线性表(如vector或list),只要提供堆栈的入栈、出栈、栈顶元素访问和判断是否为空的操作即可。由于堆栈的底层使用是其他容器,因此,堆栈可看作是一种适配器,将一种容器转换为另一种容器。

5 priority_queue优先队列容器

优先级队列也是一种从一端入队,另一端出队的队列。不同于一般队列的是,队列中最大的元素总是位于队首位置,因此,元素的出队并非按照先进先出的要求,而是将当前队列中最大元素出队。

C++ STL优先队列的泛化,底层默认采用vector向量容器,使得优先级队列的元素可做数组操作,从而应用堆算法找出当前队列最大元素,并将它调整到队首位置。

优先级队列也可以看做容器适配器,将底层的序列容器vector转换为优先队列。

6 string字符串容器

C语言并没有提供一个专门的字符串类型,需要通过字符数组,对字符串进行存储和处理。在标准C++中,字符串类string由C++ STL实现,提供丰富的字符串处理功能。string是一个基于字符的序列容器,具有vector向量一样的内部线性结构,字符逐一写入容器,最后以null字符结束。STL的string就是用字符数组实现的。

7 bit_vector位向量容器

bit_vector位向量容器是一个bit位元素的序列容器,具有vector容器一样的成员函数,常用于硬件端口的控制。区别于vector< bool>的一个重要特性是bit_vector更节省内存空间,一个元素只占用一个bit,而不是一个字节。bit_vector用vector作后缀名,实际上与vector并没有任何关联。其内部是用位数组实现的。需要指出的是,bit位的随机读写,利用C++的bit位操作运算,如位与、位或和异或等。相对比vector麻烦。

8 set、multiset、map、multimap

这四位是用红黑树实现的,红黑树保证了对于元素的插入,查找,删除都是O(logn)的时间复杂度。使用时,前两者引入头文件#include< set>,后两者引入头文件#include< map>。

9 hash_set、hash_map

这两位是用哈希表实现的,从而实现了更快的检索。这两位目前还不是C++ STL的标准容器。

算法(Algorithm)

STL的算法和容器是两个最关键的组件,其他组件都是围绕它们进行开发或使用的附带产物。C++ STL的算法都是以模板函数的方式提供出来的,可对具有泛型数据结构的容器进行数据处理。

如同容器一样,将常用的排序、交换、查找和搜索等算法处理,以泛化算法实现到C++ STL库中,避免不必要的重复设计。C++ STL的算法函数有几十种之多,分为不修改容器数据的遍历、查找、搜索、计数、匹配等非变易算法,复制、替换、交换、反向排列等变易算法,排序算法和数值计算的算法等。

迭代器(Iterator)

传统上读写数据结构中的数据,一般是通过移动读写指针来进行的。在STL中,对容器的数据读写,通过迭代器来进行,每个容器都有自己对应的迭代器。迭代器是指针的一个泛化,在容器和算法之间充当桥梁的作用,是STL泛型库最核心的一个组成部分。

内存分配器(Allocator)

STL的内存分配器是一些用于内存管理的模板类,可支持搞层次和高性能的内存管理。因为内存管理往往是项目中非常重要的一个功能部分,所以STL把这部分提取出来,方便各种容器对元素数据进行内存管理,为容器很提供了高级形式的调用。

概念(Concept)和模型(Model)

STL通过模板来传递类型参数,导致算法函数无法使用C++本身的类型检查机制,在代码的开发阶段不容易发现错误。一个较为有效的方法是,为泛型库算法函数的所有模板类型,从算法的实现步骤中归纳出相应的一个概念,用概念来指出用于具体实现的类型必须满足的运算条件,二满足某个概念所定义条件的类型,则称为该概念的一个模型。

STL的版本

HP STL:C++ STL第一个实现版本

SGI STL:最好的源代码阅读版本

STLport:支持hash_set,hash_map

P.J.Plauger STL:被微软所采用
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  STL c++