导读:作者通过侯捷的讲座,总结了一些学习心得,对一些常用的代码和设计做了非常通俗易懂的剖析,非常有帮助。文章内容如下:
很高兴侯捷老师又来公司了,给我们上了四天非常生动的技术讲座,受益匪浅,现在我简要介绍一下我的学习心得,与大家分享。这次讲座主要集中在《C++/OOP/GP/DP 》主题,针对有一些编程基础的工程师,对一些常用的代码和设计做了非常通俗易懂的剖析,非常有帮助。当然更深入的理解还需要结合多种技术名著来学习,我结合我的理解以及自己的学习和开发的经验介绍一下C++/OO/Template 以及Design Pattern的理会,考虑到讲座的性质,我并不直述本次讲座的内容,欢迎批评指正。
侯捷老师的讲座基本是讲述他多年来在C++领域的研究成果,基本大部分都可以在他的书籍和网站上能读到,但是考虑到最近几年软件技术的蓬勃发展,如Design Pattern的更广泛应用,又有许多心得,基本上是较为泛的基础的层面,并结合实际代码和应用,对实际项目开发非常有益。下面我逐个主题泛泛地讲一遍。
面向对象中的合成(Composition)和继承(Inheritance)关系
通常扩展一个类的功能主要有两种方式,一种是大家很熟悉的继承(inheritance),另一种就是合成(composition),很多初学OO(面向对象)并有一些经验都很容易搞混这个的区别,其实很简单,继承是解决Is-a的问题,而合成是解决Has-a的问题。比如说小鸟有两个翅膀,就是合成,而鸟是一种飞禽,就是继承了,设计一个“小鸟”的类,它继承自”飞禽”,就具有“飞”的特性,但要用合成的方法“包含”一个“翅膀”的类才具有真正“飞”的功能。
别看这两个定义很简单,其实很多人都犯过错误,包括Java类库的设计者,他们就把Properties直接“继承”自Hashtable了,这里其实应该用“合成”。
讲到合成,就应该说说聚合(Aggregation),它是描述整体和局部的关系,合成其实是一种“强烈”的聚合,它与局部具有相同的生命周期,“容纳”局部的“对象”,而聚合只是“容纳”局部的一个“指针”。比如说,人和脑袋就是合成,而汽车与发动机就是聚合,改装汽车可以任意替换更好的发动机,而人的脑袋就不行(目前是这样:)
聚合在UML中是以空心棱形的箭头表示,合成是以实心棱形的箭头表示。
还有一种关系叫委托(Delegation),委托是一种让合成(composition)变得像继承(inheritance)的复用能力一样强大的方式。(a way of making composition as powerful for reuse as inheritance [Lie86, JZ91] )在委托中,两个对象在处理一个请求的时候发生关联:一个接收的对象委派操作给它的委托对象。这跟子类(subclass)延迟请求(deferring requests)给它的父类(parent class)来实现类似。但是在继承里,一个被继承的操作(inherited operation)通过this成员变量能够经常引用到那个接收的对象。为了在委托里达到同样的效果,接受者传递它自己给它的委托者,以便被委托的操作能够引用到这个接收者。
再说一下继承(Inheritance),它是将基类(base-class)所有一切(包括 private )都继承下来,所以假如你想实现一个新的类,只想继承一部分,就用合成(Composition)别用继承。或者更进一步来讲,如果你想改造一个类,想改造一些接口(interface),也建议用合成,通过转调内部对象的方法实现,别用虚函数(virtual function)。这是非常符合最基本的OCP设计原则(Open-Closed Principle ,开闭原则)的方式了。
类的构造(Constructor)和析构(Destructor)
类的构造和析构是最基础的知识了,任何一个类的对象产生和销毁都必须有这两个步骤,但是它们是如何工作的,编译器是如何制造缺省的ctor和dtor的,估计少有人关注了。
一个类的对象的产生,会依次从它最里面的类开始构造,同一个类会跟据内部类成员定义的顺序依次构造。类对象的销毁的过程则相反。基类的构造器会在用户定义的ctor之前调用,基类的析构则是在用户定义的dtor之后进行。熟悉这些过程,非常有利于设计出优秀的类库,也不容易出现内存泄露和资源耗尽等问题。下面举个例子更容易理解:
class A { public: A(); ~A(); }; class B { public: B(); ~B(); }; class C { public: C(); ~C(); }; class D : public A, B { public: D() { init(); } ~D() { release(); } private: void init(); void release(); C c; }; |
上面的定义中D类的ctor构造过程如下:
A::A(); B::B(); c.C::C(); D::init(); |
D类的dtor析构过程如下:
D::release(); c.C::~C(); B::~B(); A::~A(); |
更复杂的继承关系以及多重继承的构造和析构过程类似,有兴趣的人可以写程序测试:)
还有一个问题,编译器会在什么时候自动产生ctor和dtor的呢,又是如何产生的呢
其实很简单,当你没有写缺省构造函数(default constructor)和缺省析构函数(default destructor)的时候,编译器就会给你自动生成一个,换句话说,任何类都有构造函数和析构函数,虽然有时候什么都不做,还有复制构造函数(copy ctor)也会自动生成。但是如何产生会跟你的类的成员有关。如果成员都是原生类型,还有如果类成员也全部为原生类型,ctor将只会跟普通变量定义的初始化一样,给一个初值,dtor则什么都不做,copy ctor则会使用内存复制(memcpy)的方式复制对象。如果成员包含一个或多个类成员,而且至少有一个类成员定义有缺省构造方法,则产生的ctor会依次调用每个成员的ctor。dtor和copy-ctor产生方法类似。(详见《Inside the C++ Object Model》)
多态(Polymorphism)和虚函数(Virtual function)
多态是面向对象的基本特性,C++里是通过virtual关键词来提供的,它是通过在类对象里加入vtbl虚函数表来实现的,这一点相信大部分程序员都很清楚,不过怎么做到多态功能估计了解的不多了。要详细了解,还请阅读《 Inside the C++ Object Model》一书,下面简单介绍一下原理。
一般编译都会给包含有virtual function的类头部(有的编译也会放到底部,比如 VC )增加一个成员vptr指针,指向一个 vtbl 虚函数表,为定长数组,大小是所有带virtual的函数数目再加 1 。虚函数指针从 vtbl[1] 开始,按照定义顺序,指向特定的函数实现。如果子类定义了父类中带virtual的函数,则vtbl相应的指针指向子类的函数实现,否则就指向父类的实现。另外再说明一点,其中vtbl[0]是有别的用途,用来存放类型信息,做dynamic_cast 用途。
仍以上面的例子为例,如下的代码编译器是如何处理:
A *p = new D(); // up-cast
p->vfunc1(); // 编译器会转化为如下代码
(*(p->vptr))[n](p); // n 为编译期确定的固定数,即相应 virtual function
// 所在位置
需要牢记一点,总是让 base class 拥有 virtual destructor 。因为当如下操作时
delete p;
如果A和B的析构函数不是虚函数,则不会调用子类D的dtor,就有可能造成内存泄露或者资源没有释放等严重问题。如果给base class加了virtual dtor,由于有多态的特性,就会自动调用subclass的dtor,接下来就会上面的介绍,依次调用各个base class的 dtor,因而就没有问题了。
C++ template即模板技术是实现泛型编程技术的,能够使得写一份代码可以应用到类似用途的不同地方。模板技术其实原理比较简单,但是使用还是比较复杂的,看看 STL 源码就知道了,如果还不相信,再看看Boost代码好了,会把你搞得晕头转向。候捷老师把这个技术讲解得非常清楚易懂,还具体分析了 STL 里各个大组件的运作原理,我这里就不讲述了,基本都是源码的剖析,请阅读候捷老师的《STL源码剖析》一书。
C++template和STLcontainers
在讲解STL中用模板如何实现function class(实现函数功能的类,在stl_functions.h)中,有这样一段代码
template <class _Operation> class binder1st : public unary_function<typename _Operation::second_argument_type, typename _Operation::result_type> { protected: _Operation op; typename _Operation::first_argument_type value; public: binder1st(const _Operation& __x, const typename _Operation::first_argument_type& __y) : op(__x), value(__y) {} typename _Operation::result_type operator()(const typename _Operation::second_argument_type& __x) const { return op(value, __x); } }; |
有人提出上面_Operation op;为什么不定义为引用,如_Operation &op;呢。我的想法如下,因为构造方法为
binder1st(const _Operation& __x, // 这里为const类型
const typename _Operation::first_argument_type& __y)
传入的参数为const类型,这时不应在本调用方法(这里是构造方法)之外使用引用或指针指向它,因为带const T &t的参数一般情况都视为临时对象,很有可能是在方法调用的时候临时产生的,比如说自动转型产生的临时对象都是const T &类型,它的生命周期都在此方法调用期间内,方法调用结束即被销毁,所以就不能在方法外部用引用或指针之类指向它了。举例来说,可能比较容易理解,比如大家常用的string类,假如有一个方法和调用如下:
void func(const string &s);
func("abcdfd");
这个时候就会出现自动转型行为,编译器会做如下处理
func(string("abcdfd"));
即产生一个临时的string对象,这个对象是以const类型传入的。假如你的方法定义改成如下
void func(string &s);
现在大部分编译器严格的处理都会报错,以前的VC6就不会,但是好像最新的VC2005 也报错了。
这是其中一个原因,还有一个原因我认为是_Operation类只是一个 function class,没有成员,所以做复制构造也不会有多大的开销,基本不会影响效率。再加模板和inline方法的处理,编译器经过优化,应该都不会产生临时对象了,所以也不必用引用了。不过我觉得最重要是上面第一个原因。
内存池和小对象分配器(memory pool,small object allocator)
候捷老师在内存池方面也有很丰富的研究经验,他基本将目前主流的内存池实作都剖析了一遍,介绍了它们各自的特点,以及如何与上层框架的配合。内存池是一个非常基础也非常关键的底层库,一般大型的框架自己都带有一个内存池库,比如STL、MFC等。即使在目前内存比较便宜的今天,内存资源也是最宝贵的系统资源之一,设计一个优秀的内存池对提高系统的效率和稳定性都非常有帮助,尤其是设计专门针对小内存对象(一般低于128字节)的分配器非常重要,因为这样对象分配和释放非常频繁,只用简单的malloc()和free()来处理非常影响效率,不是一个优秀的设计。下面我简要介绍一下目前主流内存池设计的特点,以及我自己的想法,另外再加一个候捷老师没提到ACE中的内存池管理器的设计特点。
SGI STL中的内存分配器(allocator)
SGI STL 的 allocator 应该是目前设计最优秀的 C++ 内存分配器之一了,它的运作原理候捷老师在《STL源码剖析》里讲解得非常清楚。基本思路是设计一个free_list[16]数组,负责管理从8 bytes到128 bytes 不同大小的内存块(chunk),每一个内存块都由连续的固定大小(fixed size block)的很多chunk组成,并用指针链表串接起来。比如说
free_list[3]->start_notuse->next_notuse->next_notuse->...->end_notuse;
当用户要获取此大小的内存时,就在free_list的链表找一个最近的free chunk回传给用户,同时将此chunk从free_list里删除,即把此chunk前后chunk指针链结起来。用户使用完释放的时候,则把此chunk放回到free_list中,应该是放到最前面的start_free的位置。这样经过若干次allocator和deallocator后,free_list中的链表可能并不像初始的时候那么是chunk按内存分布位置依次链接的。假如free_list中不够时,allocator会自动再分配一块新的较大的内存区块来加入到free_list链表中。
可以自动管理多种不同大小内存块并可以自动增长的内存池,这是 SGI STL 分配器设计的特点。
Loki中的小对象分配器(small object allocator)
Loki的分配器与 SGI STL的原理类似,不同之处是它管理free_list不是固定大小的数组,而是用一个vector来实现,因此可以用户指定fixed size block的大小,不像 SGI STL是固定最大128 bytes的。另外它管理free chunks的方式也不太一样,Loki是由一列记录了free block位置等信息的Chunk类的链表来维护的,free blocks则是分布在另外一个连续的大内存区间中。而且free Chunks也可以根据使用情况自动增长和减少合适的数目,避免内存分配得过多或者过少。
Loki的分配器使用也不太一样,可以直接调用,如下
SmallObjAllocator myAlloc(2048, 256); // 参数 1 为 chunk size
// 参数 2 为 max fixed size block size
// 可以用于小于 256 bytes 的各种大小内存的分配
void *p1 = (void*)myAlloc.Allocate(20);
void *p2 = (void*)myAlloc.Allocate(100);
void *p3 = (void*)myAlloc.Allocate(256);
void *p4 = (void*)myAlloc.Allocate(300); // 大于 256 将转交给系统处理
myAlloc.Deallocate(p1,20);
myAlloc.Deallocate(p2,100);
myAlloc.Deallocate(p3,256);
myAlloc.Deallocate(p4,300);
MFC的CPlex和CPtrList(扮演memory pool角色)
CPlex任务比较简单,只负责管理一大块memory并串接起来,用户每次获取都返回一大块。分割由使用者(如Collection classes , CFixedAlloc)将这一大块切割为一个个小的内存块。
CPtrList 则负责管理这些切割后的小内存块,这一点有点类似 Loki 中的 free Chunks ,不过要简单多了。
MFC还有一个类叫CFixedAlloc,它是提供给应用类来分配固定大小(根据具体应用类的大小)的内存分配器。通过在应用类中定义 DECLARE_FIXED_ALLOC(Foo)和IMPLEMENT_FIXED_ALLOC(Foo)两个宏来实现。
Boost的object_pool
Boost中的object_pool也是一个可以根据用户具体应用类的大小来分配内存块的,也是通过维护一个 free nodes 的链表来管理的。可以自动增加 nodes块,初始是32个nodes,每次增加都以两倍数向 system heap 要内存块。 object_pool 管理的内存块需要在其对象销毁的时候才返还给 system heap 。
ACE 中的 ACE_Cached_Allocator 和 ACE_Free_List
ACE 框架中也有一个可以维护固定大小的内存块的分配器,原理与上面讲的内存池都差不多。它是通过在ACE_Cached_Allocator中定义个Free_list链表来管理一个连续的大内存块的,里面包含很多小的固定大小的未使用的区块(free chunk),同时还使用ACE_unbounded_Set维护一个已使用的chuncks,管理方式与上面讲的内存池类似。也可以指定chunks的数目,也可以自动增长,定义大致如下所示:
template<class T> class ACE_Cached_Allocator : public ACE_New_Allocator<T> { public: // Create a cached memory pool with @a n_chunks chunks // each with sizeof (TYPE) size. ACE_Cached_Allocator(SIZET n_chunks = ACE_DEFAULT_INIT_CHUNKS); T* allocate(); void deallocate(T* p); private: // List of memory that we have allocated. Fast_Unbounded_Set<char *> _allocated_chunks; // Maintain a cached memory free list. ACE_Cached_Free_List<ACE_Cached_Mem_Pool_Node<T> > _free_list; }; |
设计模式
最后一个主题重点讲讲设计模式,设计模式现在已经应用很广泛了,可以说是无处不在。设计模式现在对程序员是非常的重要,甚至到了不懂设计模式就不算真正的程序员一样。不过设计模式却又是非常高阶的理论,需要有多年的编程经验才能真正领悟,所以学习起来非常头痛。因为它道理非常简单,但是却非常抽象,候捷老师通过一大堆实际案例给我们逐个讲述了几个常用的模式的区别和用法。设计模式最经典最权威当属著名的有字天书 GoF 的《 Design Patterns 》了,我结合自己学习和实践的体会介绍一下几个模式。
结构型模式之Composite(合成模式)
GoF的定义:Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects.翻译为中文大致意思是:将对象 (s) 组成为树状结构,用以表示“局部 - 整体”的层次体系,使得让 clients 可以以一致的方式对待“单个对象”和“合成对象”。
比较典型的例子就是文件系统中“文件”和“目录”的关系,还有Windows窗口系统也是,在一个窗口中还可以开另一个窗口,多个窗口组合成的窗口还可以当作一个窗口放入另一个窗口中,比如在Word中打开多个文档就是这种情况。Composite模式的好处就是使得clients调用简单,可以用一致的接口处理单个对象或者多个单一对象组合成的对象。
实例:Java swing library 中 Component , Label , Container就是Composite模式的应用。其中 Label 和 Container 都继承自 Component ,但是Container中只是一个存放Component的数组,所以Container中就可以放很多Component,比如ScrollPane就是继承自Container,它可以放Label,还有List,Scrollbar等等,甚至还可以放一个ScrollPane,所以就达到了Composite模式的效果,简化了 client的使用。
结构型模式之 Decorator(装饰模式)
GoF 的定义: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. 翻译为中文大致的意思是:以动态的方式给一个对象添加一些额外的职责,使得不必进行 subclassing 就能扩展功能。
Decorator模式与Composite 模式的区别就是它只内含一个 component object field ,而 Composite 则内含一个 collection of component field 。 Decorator 负责将一个对象“装饰”起来,做一些“改造或者扩展”,提供额外的功能,它只针对一个 class 。而 Composite 是一组“类似”的对象及其容纳它们的容器一视同仁,使得 client 更简单地处理单个对象和一组对象。它们目的不一样。
实例: Java IO library 中 BufferedReader , Reader 之间使用的就是 Decorator 模式,其中 BufferedReader 继承自 Reader ,同时它内部含有一个 Reader 引用,它是通过另一个 Reader 对象构造而来,因此就为 Reader 提供更多的功能,如带缓冲的 Reader 。使用非常简单,只需要如此定义:
Reader in = new BufferedReader(new FileReader("test.txt"));
就为文件读取增加了带缓冲的 IO 功能,非常方便。还可以多个 Decorator 的类组合使用,可以提供更强大的功能,多使用一下 Java IO library 就会体会到。
行为模式之Observer(观察者模式)
GoF的定义: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
翻译为中文大致意思是:在 objects 之间定义一个“一对多”的依赖关系,使得当这个 object 改变状态时,所有依赖它的 objects 都能获得通知并自动更新。
Observer是用于做“通知”用途的,就像“ publish-subscribe ”,它能够做到注册需要通知的对象,并自动通知它们来更新,它们都是被动地被通知,而不是主动观察。
实例:MFC 里 CView 和 CDocument 之间就是一个观察者模式, CView 是 Observer 即观察者, CDocument 是 Observable 即被观察者,当 CDocument 修改后会自动通知所有的 CView 对象来自动更新它们显示的内容,这一点可以用 Word 很容易试出来。还有最新的 Windows 图形界面框架 WinForm 中的窗口之间消息传递等用的也是 Observer 模式,一个窗口发生改变时会自动通知所有与它有关系的窗口,来自动更新信息等,这一点 Jeffrey Richter 可以作证 J
行为模式之Template Method(模板方法)
GoF的定义:Define the skeleton of an algorithm in an operation, deferring somesteps to subclasses. Template Method lets subclasses redefine certain steps ofan algorithm without changing the algorithm's structure. 翻译为中文大致意思是:定义一个算法的骨干,延缓其中某些步骤以便在 subclasses 中定义它们。 Template Method 使得 subclasses 在不改变算法的体系结构的前提下得到重新定义算法内的某些步骤。
Template Method其实最常用了,在C++里的应用就是使用 virtual function 实现的,给一个base class 定义一个virtual方法,但是不实现,而是在它的subclasses 中实现它,这就是Template Method。这个模式的好处显而易见,就是在你设计base class的时候并不知道这个方法具体如何实现,但是却需要在base class里某个地方需要调用它以完成一个完整的算法,这时候就是需要用这个模式了。
实例:例子太多了,到处都是。
行为模式之Strategy(策略模式)
GoF的定义: Define a family of algorithms, encapsulate each one, and make theminterchangeable. Strategy lets the algorithm vary independently from client that use it. 翻译为中文大致意思是:定义一族算法,把每个算法封装起来,并使它们可以相互替换。 Stategy 使得算法给 client 使用的时候变得完全独立。
Strategy模式完全符合 OCP ( Open-Closed Principle ,开闭原则),可以在不需要做 subclass 更不需要修改 base class 的情况下在 Runtime 的时候扩展系统的功能,它不像 Template Method 需要 subclass 才能扩展新的功能。
实例: Strategy 典型的应用是在薪资系统中,当定义 Employee 类的时候,如果使用 Template Method 模式,就无法更改员工领薪资的方式等,比如员工晋升的时候。这时候就需要用 Strategy 模式,在 Employee 中定义一个指针指向具体的 PaymentMethod 对象,如果需要更改领薪资的方式的时候,只需要将它指向新的 PaymentMehtod 实现就可以了。
结构模式之Adapter(适配器模式)
GoF的定义: Convert the interface of a class into another interface clients expect.Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. 翻译为中文大致意思是:将一个 class 的接口转换为另外一种 clients 所期望的接口,使得这些 classes 可以更好地协同工作起来,而不至于因为别的不兼容的问题所影响。
Adapter 模式属于结构型模式,需要有 Adaptee (被适配者)和 Adaptor (适配器)两个对象。一般情况下,我们通过继承( Inheritance )或者合成( Composition )的方式扩展类的功能后,会产生很多接口形式不同的类,但是我们想用同样的方式调用它们,又不想改造它们(否则违反 OCP 原则),怎么办呢?这种情况下就需要用到 Adapter 模式了,这些被“改造”的类就叫做 Adaptee ,而我们就需要写个 Adaptor 类来转调它们,把调用的接口改成统一的形式。
实例:Java IO library 里的InputStreamReader 就是一个 Adapter ,它负责将 InputStream 对象转换为 Reader 对象的接口形式,使得用户可以像使用其它 Reader 一样的方式来使用InputStream对象,比如:
InputStreamReader isr = new InputStreamReader( new FileInputStream("test.txt")); BufferedReader br = new BufferedReader(isr); String line = br.readLine(); |
这就做到了在不改造原来设计的类的接口的情况,扩展了类的应用范围。一般情况下, Adapter 模式需要结合 Factory 模式和 Proxy 模式来使用,使得接口的访问更加一致性,更容易改造系统。
结构模式之Facade(外观模式)
GoF的定义: Provide a unified interface to a set of interfaces in a subsystem.Facade defines a higher-level interface that makes the subsystem easier to use. 翻译为中文大致意思是:为一个子系统的一批接口提供一个统一标准的接口, Facade 定义更高层次的接口,使得子系统更容易使用。
Facade 模式我感觉是一个更高层次的 Adapter ,它是用来理顺系统之间的关系,降低系统间的耦合度的常用方法,其实我们可能在设计系统的时候就在不知不觉地应用这个模式了。大部分将业务逻辑层和表示层分离的框架都是应用 Facade 模式实现的,使得上层使用者完全不必理会底层的实现,却又有统一的使用界面。
实例: J2EE EJB 中 Session Bean 就是一种典型的 Facade 模式,即 Session Facade 。它完全将业务对象封装起来, EJB 客户端访问 Session Bean 来代替访问业务对象。这就大大简化了 EJB 的系统结构, Session Bean 就是相当于一个外观,也相当于一个总管,把业务对象都管理起来,不让客户端直接“接触”到它们。
《设计模式》一书中还有很多模式这里就不一一介绍了。其实我们大家都可以自己设计一种模式,但是肯定没有 GoF 的那 23 个著名了。不过现在还有很多别的也很有名的模式,比如说 Wrapper 模式(相当于 Decorator )、 DAO 模式( Data Access Object 模式, OR 映射系统里常用到)、 MVC 模式(著名的 Model-View-Control 架构模式)等等。我的一个观点,只有大量地实践,持续地学习,就会不断提升自己设计模式的层次,如果想靠一两年的学习就想精通它,是不可能的任务 J
写了这么多,终于写完了,呵呵。写完后,有两点体会: 1, 写源码剖析类的文章,就像是给代码做翻译,把代码翻译成图片或文字。 2 ,写 OO/DP 之类理论的文章,则相当于创造,不同的人有不同的体会,技术学习层面不同的理解也不同。还是那句话:理论需要从实践中来,多学习多实践。欢迎指正!
最后我列几本候捷老师推荐的书:
1 , C++ 程序员必备的书(我以为,即时你不看,也建议备上,充门面也好,争吵也好都很用)
a) 《 C++ Programming Language 》 Bjarne Stroustrup
b) 《 C++ Primer 》 Stanley B Lippman
c) 《 Effective C++ 》 Scott Meyers
d) 《 Design Patterns 》 GoF
2, 提高或学习的书
a) 《 Inside The C++ Object Model 》 Stanley B Lippman
b) 《 Thinking in C++ 》 Bruce Eckel
c) 《 More Effective C++ 》 Scott Meyers
d) 《 STL 源码剖析》侯捷
e) 《 Modern C++ Design 》 Andrei Alexandrescu
f) 《 Small Memory Software - Patterns for memory management 》
原文链接:http://www.cppblog.com/javenstudio/articles/17057.html