本文涉及到 SGI STL 源码的文件有liststl_list.hlist.h 等几个文件。

1. list 和 slist

STL中也实现了链表这种数据结构,list是STL标准的双向链表,而slit是SGI的单链表。相比于vector的连续线性空间而言,list即有有点也有缺点:优点是空间分配更灵活,对任何位置的插入删除操作都是常数时间;缺点是排序不方便。list和vector是比较常用的线性容器,那么什么时候用哪一种容器呢,需要视元素的多少、元素构造的复杂度(是否为POD数据)以及元素存取行为的特性而定。限于篇幅,本文主要介绍list的内容,关于单链表slist可以参见源码和侯捷的书。

2. list 的数据结构

在数据结构中,我们知道链表的节点node和链表list本身是不同的数据结构,以下分别是node和list的数据结构:
cpp struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_prev; }; template <class _Tp> struct _List_node : public _List_node_base { // node 的定义 _Tp _M_data; }; template <class _Tp, class _Alloc> class _List_base { protected: _List_node<_Tp>* _M_node; // 只要一个指针就可以表示整个双向链表 }; template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) > class list : protected _List_base<_Tp, _Alloc> { public: typedef _List_node<_Tp> _Node; };

本文涉及到 SGI STL 源码的文件有vectorstl_vector.hvector.h 等几个文件。

1. 容器

在数据结构的课程中,我们主要研究数据的特定排列方式,以利于搜索、排序等算法,几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL 容器即由一个个特定的数据结构组成,例如向量(vector),链表(list),堆栈(stack),队列(queue),树(tree),哈希表(hash table),集合(set),映射(map)等,根据数据在容器中的排列特性,这些数据接口分为序列式容器(sequence container)和关联式容器(association container)两种,本文主要解读SGI STL中的序列式容器。

所谓序列式容器,其中的元素可序(ordered),但未必有序(sorted)。C++ 本身提供了一个序列式容器——数组(array),STL中还提供了向量(vector),链表(list),堆栈(stack),队列(queue),优先队列(priority queue)等,其中stack和queue只是将deque(双端队列)设限而得到的,技术上可以被归为一种配接器(adaptor)。本系列文章将依次解读SGI STL各容器的关键实现细节。

2. vector 及其数据结构

在STL中,vector的空间在物理上就是连续的,而且是可以动态扩展的,这里的动态扩展,不需要用户去处理溢出的问题,而只需要关心上层逻辑。vector连续物理空间的动态扩展技术是该容器的关键,它主要分为三个步骤:配置新空间,数据移动,释放旧空间。这三个步骤执行的次数以及每次执行时的效率是影响最终 vector 效率的关键因素。为了减少执行的次数,就需要未雨绸缪,每次扩充空间时,成倍增长。而每次执行的效率,就主要是数据移动的效率了。下面,我们依次介绍vector的数据结构,使用的空间配置器和迭代器,以及常用操作。
vector 的数据结构
vector的数据结构很简单,就是一段连续的物理空间,包含起止地址以及已用到的空间的末尾地址这三个成员:
cpp template <class _Tp, class _Alloc> class _Vector_base { protected: _Tp* _M_start; _Tp* _M_finish; _Tp* _M_end_of_storage; }; class vector : protected _Vector_base<_Tp, _Alloc>{ };

本文涉及到 SGI STL 源码的文件有 iterator.h, stl_iterator_base.h, concept_checks.h, stl_iterator.h, type_traits.h, stl_construct.h, stl_raw_storage_iter.h 等7个文件。

1. 迭代器的设计思维

迭代器(iterators)是一种抽象的设计概念,显示程序中并没有直接对应于这个概念的实体。在 Design Patterns 一书中,对 iterators 模式的定义如下:提供一种方法,使之能够依序遍历某个聚合物(容器)所包含的各个元素,而又无需暴露该聚合物内部的表述方式。

在STL中迭代器扮演着重要的角色。STL的中心思想在于:将数据容器(container)和算法(algorithm)分开,彼此独立设计,最后再通过某种方式将他们衔接在一起。容器和算法的泛型化,从技术的角度来看并不困难,C++ 的 class template 和 function template 可以分别达到目标,难点在于如何设计二者之间的衔接器。

在STL中,起者这种衔接作用的是迭代器,它是一种行为类似指针的对象。指针的各种行为中最常见也最重要的便是内容获取(dereference)和成员访问(member access),因此迭代器最重要的工作就是对 operator*operator-> 进行重载。然而要对这两个操作符进行重载,就需要对容器内部的对象的数据类型和存储结构有所了解,于是在 STL 中迭代器的最终实现都是由容器本身来实现的,每种容器都有自己的迭代器实现,例如我们使用vector容器的迭代器的时候是这样用的 vector<int>::iterator it; 。而本文所讨论的迭代器是不依存于特定容器的迭代器,它在STL中主要有以下两个方面的作用(我自己的理解和总结):

  • 规定容器中需要实现的迭代器的类型及每种迭代器的标准接口
  • 通过Traits编程技巧实现迭代器相应型别的获取,弥补 C++ 模板参数推导的不足,为配置器提供可以获取容器中对象型别的接口

其中前一个没啥好解释的。关于第二个,后面第3节会详细介绍,那就是Traits编程技巧。

在STL中,Memory Allocator 处于最底层的位置,为一切的 Container 提供存储服务,是一切其他组件的基石。对于一般使用 STL 的用户而言,Allocator 是不可见的,如果需要对 STL 进行扩展,如编写自定义的容器,就需要调用 Allocator 的内存分配函数进行空间配置。本文涉及到的 SGI STL 源代码文件有 alloc.h, stl_config.h, stl_alloc.h, stl_threads.h 这4个。

在C++中,一个对象的内存配置和释放一般都包含两个步骤,对于内存的配置,首先是调用operator new来配置内存,然后调用对象的类的构造函数进行初始化;而对于内存释放,首先是调用析构函数,然后调用 operator delete进行释放。 如以下代码: cpp class Foo { ... }; Foo* pf = new Foo; ... delete pf; Allocator 的作用相当于operator new 和operator delete的功能,只是它考虑得更加细致周全。SGI STL 中考虑到了内存分配失败的异常处理,内置轻量级内存池(主要用于处理小块内存的分配,应对内存碎片问题)实现, 多线程中的内存分配处理(主要是针对内存池的互斥访问)等,本文就主要分析 SGI STL 中在这三个方面是如何处理的。在介绍着三个方面之前,我们先来看看 Allocator的标准接口。

0. 两个问题

在介绍 STL 之前,先讨论两个问题:为什么要剖析 STL 源代码?如何剖析 STL 源代码?

首先是为什么要剖析 STL 源代码呢? 有人会说,会使用 STL 不就性了,为什么一定要知道其内部的机制呢? 对于大多数程序猿来说,确实没有必要去阅读或分析 STL 的源代码,但如果要想提升自己的编程修养,要相让自己编码的思想境界提升一个档次,还是很有必要读读 STL 这样的大师制作。阅读和分析之后,你会明白STL是如何分配和管理内存的(特别是对vector、string、deque等动态数据结构),是如何实现各种数据结构(特别是红黑树等比较复杂的数据结构)和相关算法的,又是如何将这些组件融合起来实现高内聚低耦合的。或许用《洋葱》的几句歌词获取最能表达你的明白这些问题之后的心情:

如果你愿意一层一层
一层的剥开我的心
你会发现
你会讶异
你是我
最压抑
最深处的秘密

如果你愿意一层一层
一层的剥开我的心
你会鼻酸
你会流泪
只要你能
听到我
看到我的全心全意

那么,如何剖析源代码呢?其实歌词中已经蕴含这答案了,那就是“一层一层一层的剥开”。当然,我这里说的一层一层不是说要一个函数step in 到底,而是说要按层次解读:首先从最外层结构框架着手,从整体上把握;然后从细处着笔,一个组件一个组件的来分析;在分析每个组件时,也是先把握改组件的全貌及其与其他组件的关联关系,然后在深入组件内部,了解其实现。在阅读和分析源码的过程中,首先要理解其功能,然后在看它是如何实现的。切忌纠缠于代码的细节或陷入源码而不能自拔,即坠入“不识庐山真面目,只缘身在此山中”的深渊!

因此,本文的目的在于,站在STL这座大山的山顶,一窥其全貌。随后的文章则深入每个组件,细细观赏每一处的风景。

概述

在编程过程中,一些特殊的时候,我们需要向一个函数传递另一个函数的地址(比如在快速排序中,我们需要传入两个元素大小比较的函数的地址),此时在C语言中一般是通过传递一个函数指针来实现。最近在看《STL源码剖析》一书,上面提到,在C++中其实可以通过另一种方式实现,那就是函数调用操作符() 。本文首先介绍一下C语言中函数指针的用法,然后再介绍C++中函数调用操作符的用法。

C语言中的函数指针

我们先直接看一个例子吧。这个例子比较全面而且简单,其中的函数指针是带参数且有返回值的函数指针,而且还有把函数指针作为参数来传递的代码。这个例子来自 jobbole伯乐在线,代码如下:

概述

Robert C.SearcordThe Cert C Secure Coding Standard 一书中,关于宏定义的规范中第一条就是

用内联函数或静态函数替代与函数相似的宏

这个规范非常实用。内联函数是C99标准中新增的,当宏定义和内联函数可以互换时,应该优先考虑选择内联函数,这也是为什么在C++标准库函数中 max, min, swap 等都是通过内联函数来实现的原因。 宏定义是完全原封不动的很SB的替换,而内联函数则并非简单的文本替换,而是按函数调用的方式展开。关于内联函数相对于宏替换的优点,在wiki有如下几点的总结:

  • 宏调用并不执行类型检查,甚至连正常参数也不检查,但是函数调用却要检查。
  • C语言的宏使用的是文本替换,可能导致无法预料的后果,因为需要重新计算参数和操作顺序。
  • 在宏中的编译错误很难发现,因为它们引用的是扩展的代码,而不是程序员键入的。
  • 许多结构体使用宏或者使用不同的语法来表达很难理解。内联函数使用与普通函数相同的语言,可以随意的内联和不内联。
  • 内联代码的调试信息通常比扩展的宏代码更有用。

其中前面两条很好理解,相信大家应该不陌生,这里主要通过具体讨论一个该书中提到的一个程序实例来感受一下后面几点。

1.概述

C++ 程序的存储空间可以分为静态/全局存储区、栈区和堆区。下图展示了一个典型的Linux C/C++ 程序内存空间布局:

其中,每一部分的具体涵义如下:
- 代码段(.text):这里存放的是CPU要执行的指令。代码段是可共享的,相同的代码在内存中只会有一个拷贝,同时这个段是只读的,防止程序由于错误而修改自身的指令。
- 初始化数据段(.data):这里存放的是程序中需要明确赋初始值的变量,例如位于所有函数之外的全局变量:int val=100; 。 需要强调的是,以上两段都是位于程序的可执行文件中,内核在调用 exec 函数启动该程序时从源程序文件中读入。
- 未初始化数据段(.bss):位于这一段中的数据,内核在执行该程序前,将其初始化为0或者null。例如出现在任何函数之外的全局变量:int sum;
- 堆(Heap):这个段用于在程序中进行动态内存申请,例如经常用到的 malloc,new 系列函数就是从这个段中申请内存。
- 栈(Stack):函数中的局部变量以及在函数调用过程中产生的临时变量都保存在此段中。
静态/全局存储区和栈区一般在程序编译阶段决定;而堆区则随着程序的运行而动态变化,每一次程序运行都会有不同的行为,因此动态内存管理对于一个程序在运行过程中占用的内存大小及程序运行性能有非常重要的影响。 本文主要探讨在C++中如何管理动态内存,以及如何使用 C++ 的语言特性来提高动态内存的管理效率,减少错误的发生。

本文首先介绍一下在 Ubuntu 下如何配置 Android NDK 开发环境,然后用一个简单的 hello-jni 项目来介绍 NDK 开发流程,本文的全部代码下载链接:HelloJni.tar.gz,也可以在我的 GitHub 上下载。

1. 简介

什么是 Android NDK 呢? NDK(Native Development Kit) 是一个允许开发者用一些本地语言(C/C++)编写 Android App 的部分功能的工具集。对于一些特定的 App,NDK 非常有利于我们直接使用现成的用 C/C++ 编写的代码库(但对于大多数 App 来说,NDK 是没有必要的)。使用 NDK 进行 C/C++ Android 开发的基本结构和流程如下图(来自shihongzhi博客 ):

在开始之前,这里要提醒大家:NDK 对大多数 App 而言是不会有太多的好处的,在 Android 上使用原生 C/C++ 代码并不会很明显的提升应用的性能,但却增加了你开发应用的复杂度。所以,仅仅在需要使用 NDK 时才使用它 —— 不要因为你更喜欢用 C/C++。典型的 NDK 应用场景是一些 CPU 操作密集而不需要太多内存的场合,如信号处理、物理模拟等等,很多这些处理过程都已经封装到了 Android 系统内部,所以当你不确定是否要使用本地 C/C++ 代码时,先看看你的需求,以及 Android 框架中的 API 是否已经提供你需要的功能。