写在前面的话
C++的STL库很早以前就已经是C++的标准库了,大量的c++应用都在使用,STL是一个代码写得非常精美的库,侯捷老师有一本<<STL源码剖析>>对此有非常详细的介绍,感兴趣的可以去看一下,认真看完看懂以后,对你编程的思想还是有些影响的,毕竟读完以后看到了那么多大师的写法,怎么说脑袋还是会受到一些影响的。
看标准库的源代码,并不是要更加熟悉的使用库,而是强化自己的编程思想,像STL这样的标准库,基本上代码已经非常精简没有什么多余的了(精简的同义词实际上是非常晦涩难懂),阅读这样的代码,可以大大提供自己的编程思想,注意,是思想,不是能力,我觉得,编程能力还是得靠练习实际项目才能有提高的,当然,一些牛人除外,他们的思想和算法我们是跟不上的,比如雷神之锤中的那个浮点数开方函数中的0x5f3759df,像这玩意,我等凡人就不要去深究了。。。
还有,虽然是c++,但是STL并不是按照面向对象的方式进行编程的,所以你要是想增加自己的面向对象的功力,还是不要看这个了,并且用了比较多的模板和c++一些特别语法,读起来会非常非常晦涩,但是理解了以后就一片光明了。
我是对照侯老师的书去看的源代码,我觉得没必要非常非常深入的读,只需要理解他们的编程思想就行了,太深入的话,一是花费太多时间,二是这种精简的代码往往有很多编程技巧在里面,理解困难并且实际项目中使用得也比较少,因为在实际项目中更多的还是要跟人合作,你写的代码总得让别人看得懂吧,使用过多的编程技巧实际上是一种费力不讨好的事情。
STL的组件
STL的组件分成6大部分,分别是:
- allocators 这是一个最底层的部分,负责空间的配置,管理,申请,释放等等
- containers 容器,这个是我们用的最多的,像vector,map,set等等都在这里面
- iterators 迭代器,这个也是我们用得最多的,一般和容易一起使用
- algorithms 算法,这个用得也不少,排序,搜索,替换之类的都在这里
- functors 这个是算法的策略
- adapters 适配器,知道设计模式的就应该比较容易理解,你把它理解成接口可能更容易一些
上面六大组件就是STL的核心,我们一个一个来说一下。
allocators --- 空间配置器
简单的来说,空间配置器主要完成空间的申请和释放,在c++中,我们知道一个对象的整个生命周期包括:
- 申请类的内存空间
- 调用构造函数初始化类中的相关信息
- 类操作
- 调用析构函数释放类中间的相关信息
- 释放类的内存空间
第一和第二个操作是由new
关键字来实现的,第四和第五个操作是由delete
来实现的,在STL中,内存的申请和释放,都是通过stl_alloc.h
来实现的,对于类的初始化和析构,都是通过stl_constuct.h
来实现的,这样把内存的释放和类的内部变量释放分开了,这只是一种设计方法,没什么特别的地方,只是为了更直观好看一点。
我们分别来看看这两个文件吧,首先是stl_constuct.h
完整的文件在这里(stl_constuct.h)
首先,最基本的部分:
//带参数的构造函数 template <class _T1, class _T2> inline void _Construct(_T1* __p, const _T2& __value) { new ((void*) __p) _T1(__value); } //无参数的构造函数 template <class _T1> inline void _Construct(_T1* __p) { new ((void*) __p) _T1(); } //析构函数 template <class _Tp> inline void _Destroy(_Tp* __pointer) { __pointer->~_Tp(); }
这个很简单,就是调用传入的T的构造和析构函数而已,当然,除了这些还有些特别的,我们在使用STL的时候不一直都使用迭代器么,析构的时候也可以使用迭代器,构造的时候为什么没有用迭代器,呵呵,你对象都还没有哪来的对象数组可迭代啊。
//当第三个参数为`__false_type`时挨个调用析构方法 template <class _ForwardIterator> void __destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type) { for ( ; __first != __last; ++__first) destroy(&*__first); } //判断传入的类是否有析构方法 template <class _ForwardIterator, class _Tp> inline void __destroy(_ForwardIterator __first, _ForwardIterator __last, _Tp*) { typedef typename __type_traits<_Tp>::has_trivial_destructor _Trivial_destructor; __destroy_aux(__first, __last, _Trivial_destructor()); } //总入口,传入的是迭代器的区间,通过__VALUE_TYPE判断输入类型 template <class _ForwardIterator> inline void _Destroy(_ForwardIterator __first, _ForwardIterator __last) { __destroy(__first, __last, __VALUE_TYPE(__first)); }
_Trivial_destructor
用来查询是否是一个无关紧要的析构函数,如果是,那么析构的时候就啥也不干,提高效率。__VALUE_TYPE
用来判断传入的迭代器中元素的类型。
通过这两层封装,就实现了类的解析和析构,并且和迭代器结合起来了。当然,源代码中还有一些其他内容,感兴趣的可以去看看,比如对某些基本类型的特别优化,说是优化,其实就是啥也不干。
恩,构造和析构说完了,我们接下来看看stl_alloc.h
,完整的程序在这里stl_alloc.h
stl_alloc.h
主要用来进行内存操作,也就是内存的申请和释放,也就是我们熟悉的new
,delete
和malloc
,free
这些个函数,你可以把stl_alloc.h
看成是这些函数的封装,其实就是封装了malloc
,free
,但是封装的时候考虑了更多的问题。
这个文件比较大,首先,我们要找到总的接口,就是其他组件使用内存的时候调用的接口,是里面的simple_class
类,这个类描述如下:
template<class _Tp, class _Alloc> class simple_alloc { public: //申请内存,指定大小 static _Tp* allocate(size_t __n) { return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); } //申请类型为_Tp的内存空间 static _Tp* allocate(void) { return (_Tp*) _Alloc::allocate(sizeof (_Tp)); } //释放内存,指定个数 static void deallocate(_Tp* __p, size_t __n) { if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); } //直接释放内存 static void deallocate(_Tp* __p) { _Alloc::deallocate(__p, sizeof (_Tp)); } };
我们会发现,不管是哪个方法,调用的都是_Alloc
这个类型的方法,至于这个类型是什么,由于是使用的模板方式编写的程序,那只有传进来的时候才知道
在stl_alloc.h中,实际上使用了两层内存分配策略,第一层的策略实际上直接就是malloc和free的封装,第二层的稍微复杂一点,维护了几个链表,小型的数据申请和释放更加迅速,在代码里面分别是__malloc_alloc_template
和__default_alloc_template
对于__malloc_alloc_template
比较简单,就是封装了标准c语言的内存申请释放函数,大家可以看看源代码。
而对于__default_alloc_template
,实际上实现方式是,当申请的内存大于128字节的时候,交给__malloc_alloc_template
来处理,当小于128的时候自己处理,并且维护了一组链表,申请和释放128以下的内存直接从链表中获取和消除,省去了调用malloc和free的时间,速度更快,其实这种方法,我们自己也用得很多,这其实就是内存池了。
这些个代码我就不贴出来了,大家感兴趣可以自己研究研究。
上面就是第一个组件allocators
,这个组件实际上并没有对外开放,一般都是在STL内部使用,但是这个是STL的基石,因为不管是上面数据结构还是算法,最后都离不开内存这玩意。