在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进行释放。 如以下代码:

1
2
3
4
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 是否已经提供你需要的功能。

很长一段时间以来就看到各种基于 Twitter Bootstrap 主题的博客很清爽,而且对 Tag 的归档也做得很赞,于是很想将自己的博客也换成 Bootstrap 的主题,随着看到的博客越来越多,自己的 Octopress 主题先得越来越臃肿,而 Tags 归档功能也相形见绌,更换主题的欲望越来越强烈了。于是乎,趁这个周末捣鼓了一番,最终大功告成,在这里分享一下具体过程。

Bootstrap 主题的安装

首先下载适用于 Octopress 的 Bootstrap主题 并解压缩到博客的 .theme 目录,然后安装:

1
rake install['bootstrap']

安装的过程中可能会提示有 sass 或其他依赖库的语法错误神马的,这是因为 sass 的版本过低,可以通过如下命令来跟新:

1
bundle update sass

在Leetcode上有好几道全排列相关的题,一直以来只是会写基于递归的全排列生成算法,遇到这几道题后,搜了下一些非递归的实现方法,发现其实全排列的生成还是很有规律的有木有!这里就总结一下递归和非递归的全排列生成方法,并分析一下 STL 的实现。

递归和非递归实现全排列生成的方法也分别有多种,递归法有基于交换的,有基于链接的,还有回溯,非递归法有排序、回溯、求模等,关于所有这些方法的具体实现参见 全排列的六种算法. 本文只实现一种递归和一种非递归算法,并在最后对 STL 的非递归算法进行分析。

本文的全部代码下载:code.

递归法求全排列

递归法的基本思路是这样的:

首先选一个元素排在第一个(有 n 中选法); 然后递归的对剩下的所有元素进行全排列; 直到一个元素的全排列是其本身。

假设给定的元素序列为 <e1, e2, ..., en>,其全排列表示为 P(e1, e2, ..., en),则对递归的第一步展开有:

P(e1, e2, ..., en) = { <e1, P(e2, e3, ..., en)>, <e2, P(e1, e3, ..., en)>, ... ... <en, P(e1, e2, ..., e(n-1))> }

引子

在码代码的过程中,不经意间就会遇到需要交换两个变量的情况,一般情况下都是通过新定义一个同类型的变量来中转,但自己也知道可以不用定义新变量直接原地交换,但具体如何原地交换以及其中可能隐藏的bug却了解得不是很清楚,于是乎google了一下,发现这里面还真是有很多学问呢,这里整理和总结一下。

原地交换两个变量,最主要有加减法和异或法。

本文完整代码链接:20140411.cpp

加减法

加减法最简单、最好理解了,设待交换的两个变量分别为 a 和 b ,首先将两者的和赋给 a ;然后将 a 与 b的差赋给 b ,这样 b 就是 a 原来的值了;最后再将 a 与 b 的差赋给 a ,这样 a 就是 b 原来的值了。具体代码如下:

1
2
3
4
5
6
7
inline template <class T>
void xswap(T &a,T &b){
    a=a+b;
  //printf("a=%u\n",a);
    b=a-b;
    a=a-b;
}

概述

最近在做的一个项目,需要对大量数据进行一些基本的统计和处理,整个程序的思路很简单,但处理起来却很慢,特别是有二重循环的地方,龟速前进,眼看着16核32线程 的服务器只有一个线程被利用,束手无策。之前一直听说Go是一门对并发编程有很好的支持的语言,七牛的许牛大力推崇Go语言,于是就开始了对Go并行编程的探索之旅。