http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html?ca=drs-cn
本章首先简单介绍自定义内存池性能优化的原理,然后列举软件开发中常用的内存池的不同类型,并给出具体实现的实例。

引言

本 书主要针对的是 C++ 程序的性能优化,深入介绍 C++ 程序性能优化的方法和实例。全书由 4 个篇组成,第 1 篇介绍 C++ 语言的对象模型,该篇是优化 C++ 程序的基础;第 2 篇主要针对如何优化 C++ 程序的内存使用;第 3 篇介绍如何优化程序的启动性能;第 4 篇介绍了三类性能优化工具,即内存分析工具、性能分析工具和 I/O 检测工具,它们是测量程序性能的利器。

在此我们推出了此书的第 26 章供大家在线浏览。更多推荐书籍请访问 developerWorks 图书频道





回页首


6.1 自定义内存池性能优化的原理


书名:《C++应用程序性能优化》
作者:冯宏华、徐莹、程远、汪磊 等编著
出版社:电子工业出版社
出版日期:2007 年 03 月
ISBN:978-7-121-03831-0
购买: 中国互动出版网dearbook

推荐章节:


如 前所述,读者已经了解到"堆"和"栈"的区别。而在编程实践中,不可避免地要大量用到堆上的内存。例如在程序中维护一个链表的数据结构时,每次新增或者删 除一个链表的节点,都需要从内存堆上分配或者释放一定的内存;在维护一个动态数组时,如果动态数组的大小不能满足程序需要时,也要在内存堆上分配新的内存 空间。

6.1.1 默认内存管理函数的不足

利用默认的内存管理函数new/delete或malloc/free在堆上分配和释放内存会有一些额外的开销。

系 统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或 者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲 块。然后系统更新内存空闲块表,完成一次内存分配。类似地,在释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空 闲块合并成较大的空闲块。

默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。

可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。

默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。

6.1.2 内存池的定义和分类

自 定义内存池的思想通过这个"池"字表露无疑,应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存作为一个内存池,之后应用程序自己对内存的 分配和释放则可以通过这个内存池来完成。只有当内存池大小需要动态扩展时,才需要再调用系统的内存分配函数,其他时间对内存的一切操作都在应用程序的掌控 之中。

应用程序自定义的内存池根据不同的适用场景又有不同的类型。

从 线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;多线程内存 池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。相对而言,单线程内存池性能更高,而多线程内存池适用范围更广。

从内存池可分配内存单元大小来分,可以分为固定内存池和可变内存池。所谓固定内存池是指应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的;而可变内存池则每次分配的内存单元大小可以按需变化,应用范围更广,而性能比固定内存池要低。

6.1.3 内存池工作原理示例

下面以固定内存池为例说明内存池的工作原理,如图6-1所示。


图6-1 固定内存池
图6-1  固定内存池

固定内存池由一系列固定大小的内存块组成,每一个内存块又包含了固定数量和大小的内存单元。

如 图6-1所示,该内存池一共包含4个内存块。在内存池初次生成时,只向系统申请了一个内存块,返回的指针作为整个内存池的头指针。之后随着应用程序对内存 的不断需求,内存池判断需要动态扩大时,才再次向系统申请新的内存块,并把所有这些内存块通过指针链接起来。对于操作系统来说,它已经为该应用程序分配了 4个等大小的内存块。由于是大小固定的,所以分配的速度比较快;而对于应用程序来说,其内存池开辟了一定大小,内存池内部却还有剩余的空间。

例 如放大来看第4个内存块,其中包含一部分内存池块头信息和3个大小相等的内存池单元。单元1和单元3是空闲的,单元2已经分配。当应用程序需要通过该内存 池分配一个单元大小的内存时,只需要简单遍历所有的内存池块头信息,快速定位到还有空闲单元的那个内存池块。然后根据该块的块头信息直接定位到第1个空闲 的单元地址,把这个地址返回,并且标记下一个空闲单元即可;当应用程序释放某一个内存池单元时,直接在对应的内存池块头信息中标记该内存单元为空闲单元即 可。

可见与系统管理内存相比,内存池的操作非常迅速,它在性能优化方面的优点主要如下。

(1)针对特殊情况,例如需要频繁分配释放固定大小的内存对象时,不需要复杂的分配算法和多线程保护。也不需要维护内存空闲表的额外开销,从而获得较高的性能。

(2)由于开辟一定数量的连续内存空间作为内存池块,因而一定程度上提高了程序局部性,提升了程序性能。

(3)比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题。





回页首


6.2 一个内存池的实现实例

本节分析在某个大型应用程序实际应用到的一个内存池实现,并详细讲解其使用方法与工作原理。这是一个应用于单线程环境且分配单元大小固定的内存池,一般用来为执行时会动态频繁地创建且可能会被多次创建的类对象或者结构体分配内存。

本节首先讲解该内存池的数据结构声明及图示,接着描述其原理及行为特征。然后逐一讲解实现细节,最后介绍如何在实际程序中应用此内存池,并与使用普通内存函数申请内存的程序性能作比较。

6.2.1 内部构造

内存池类MemoryPool的声明如下:


class MemoryPool
{
private:
MemoryBlock* pBlock;
USHORT nUnitSize;
USHORT nInitSize;
USHORT nGrowSize;

public:
MemoryPool( USHORT nUnitSize,
USHORT nInitSize = 1024,
USHORT nGrowSize = 256 );
~MemoryPool();

void* Alloc();
void Free( void* p );
};

MemoryBlock为内存池中附着在真正用来为内存请求分配内存的内存块头部的结构体,它描述了与之联系的内存块的使用信息:


struct MemoryBlock
{
USHORT nSize;
USHORT nFree;
USHORT nFirst;
USHORT nDummyAlign1;
MemoryBlock* pNext;
char aData[1];

static void* operator new(size_t, USHORT nTypes, USHORT nUnitSize)
{
return ::operator new(sizeof(MemoryBlock) + nTypes * nUnitSize);
}
static void operator delete(void *p, size_t)
{
::operator delete (p);
}

MemoryBlock (USHORT nTypes = 1, USHORT nUnitSize = 0);
~MemoryBlock() {}
};

此内存池的数据结构如图6-2所示。


图6-2 内存池的数据结构
图6-2  内存池的数据结构

6.2.2 总体机制

此内存池的总体机制如下。

(1) 在运行过程中,MemoryPool内存池可能会有多个用来满足内存申请请求的内存块,这些内存块是从进程堆中开辟的一个较大的连续内存区域,它由一个 MemoryBlock结构体和多个可供分配的内存单元组成,所有内存块组成了一个内存块链表,MemoryPool的pBlock是这个链表的头。对每 个内存块,都可以通过其头部的MemoryBlock结构体的pNext成员访问紧跟在其后面的那个内存块。

(2) 每个内存块由两部分组成,即一个MemoryBlock结构体和多个内存分配单元。这些内存分配单元大小固定(由MemoryPool的 nUnitSize表示),MemoryBlock结构体并不维护那些已经分配的单元的信息;相反,它只维护没有分配的自由分配单元的信息。它有两个成员 比较重要:nFree和nFirst。nFree记录这个内存块中还有多少个自由分配单元,而nFirst则记录下一个可供分配的单元的编号。每一个自由 分配单元的头两个字节(即一个USHORT型值)记录了紧跟它之后的下一个自由分配单元的编号,这样,通过利用每个自由分配单元的头两个字节,一个 MemoryBlock中的所有自由分配单元被链接起来。

(3)当有新的内存请求到来 时,MemoryPool会通过pBlock遍历MemoryBlock链表,直到找到某个MemoryBlock所在的内存块,其中还有自由分配单元 (通过检测MemoryBlock结构体的nFree成员是否大于0)。如果找到这样的内存块,取得其MemoryBlock的nFirst值(此为该内 存块中第1个可供分配的自由单元的编号)。然后根据这个编号定位到该自由分配单元的起始位置(因为所有分配单元大小固定,因此每个分配单元的起始位置都可 以通过编号分配单元大小来偏移定位),这个位置就是用来满足此次内存申请请求的内存的起始地址。但在返回这个地址前,需要首先将该位置开始的头两个字节的 值(这两个字节值记录其之后的下一个自由分配单元的编号)赋给本内存块的MemoryBlock的nFirst成员。这样下一次的请求就会用这个编号对应 的内存单元来满足,同时将此内存块的MemoryBlock的nFree递减1,然后才将刚才定位到的内存单元的起始位置作为此次内存请求的返回地址返回 给调用者。

(4)如果从现有的内存块中找不到一个自由的内存分配单元(当第1次请求内存,以及现有的所有内存 块中的所有内存分配单元都已经被分配时会发生这种情形),MemoryPool就会从进程堆中申请一个内存块(这个内存块包括一个MemoryBlock 结构体,及紧邻其后的多个内存分配单元,假设内存分配单元的个数为n,n可以取值MemoryPool中的nInitSize或者nGrowSize), 申请完后,并不会立刻将其中的一个分配单元分配出去,而是需要首先初始化这个内存块。初始化的操作包括设置MemoryBlock的nSize为所有内存 分配单元的大小(注意,并不包括MemoryBlock结构体的大小)、nFree为n-1(注意,这里是n-1而不是n,因为此次新内存块就是为了满足 一次新的内存请求而申请的,马上就会分配一块自由存储单元出去,如果设为n-1,分配一个自由存储单元后无须再将n递减1),nFirst为1(已经知道 nFirst为下一个可以分配的自由存储单元的编号。为1的原因与nFree为n-1相同,即立即会将编号为0的自由分配单元分配出去。现在设为1,其后 不用修改nFirst的值),MemoryBlock的构造需要做更重要的事情,即将编号为0的分配单元之后的所有自由分配单元链接起来。如前所述,每个 自由分配单元的头两个字节用来存储下一个自由分配单元的编号。另外,因为每个分配单元大小固定,所以可以通过其编号和单元大小(MemoryPool的 nUnitSize成员)的乘积作为偏移值进行定位。现在唯一的问题是定位从哪个地址开始?答案是MemoryBlock的aData[1]成员开始。因 为aData[1]实际上是属于MemoryBlock结构体的(MemoryBlock结构体的最后一个字节),所以实质上,MemoryBlock结 构体的最后一个字节也用做被分配出去的分配单元的一部分。因为整个内存块由MemoryBlock结构体和整数个分配单元组成,这意味着内存块的最后一个 字节会被浪费,这个字节在图6-2中用位于两个内存的最后部分的浓黑背景的小块标识。确定了分配单元的起始位置后,将自由分配单元链接起来的工作就很容易 了。即从aData位置开始,每隔nUnitSize大小取其头两个字节,记录其之后的自由分配单元的编号。因为刚开始所有分配单元都是自由的,所以这个 编号就是自身编号加1,即位置上紧跟其后的单元的编号。初始化后,将此内存块的第1个分配单元的起始地址返回,已经知道这个地址就是aData。

(5) 当某个被分配的单元因为delete需要回收时,该单元并不会返回给进程堆,而是返回给MemoryPool。返回时,MemoryPool能够知道该单 元的起始地址。这时,MemoryPool开始遍历其所维护的内存块链表,判断该单元的起始地址是否落在某个内存块的地址范围内。如果不在所有内存地址范 围内,则这个被回收的单元不属于这个MemoryPool;如果在某个内存块的地址范围内,那么它会将这个刚刚回收的分配单元加到这个内存块的 MemoryBlock所维护的自由分配单元链表的头部,同时将其nFree值递增1。回收后,考虑到资源的有效利用及后续操作的性能,内存池的操作会继 续判断:如果此内存块的所有分配单元都是自由的,那么这个内存块就会从MemoryPool中被移出并作为一个整体返回给进程堆;如果该内存块中还有非自 由分配单元,这时不能将此内存块返回给进程堆。但是因为刚刚有一个分配单元返回给了这个内存块,即这个内存块有自由分配单元可供下次分配,因此它会被移到 MemoryPool维护的内存块的头部。这样下次的内存请求到来,MemoryPool遍历其内存块链表以寻找自由分配单元时,第1次寻找就会找到这个 内存块。因为这个内存块确实有自由分配单元,这样可以减少MemoryPool的遍历次数。

综上所述,每个内 存池(MemoryPool)维护一个内存块链表(单链表),每个内存块由一个维护该内存块信息的块头结构(MemoryBlock)和多个分配单元组 成,块头结构MemoryBlock则进一步维护一个该内存块的所有自由分配单元组成的"链表"。这个链表不是通过"指向下一个自由分配单元的指针"链接 起来的,而是通过"下一个自由分配单元的编号"链接起来,这个编号值存储在该自由分配单元的头两个字节中。另外,第1个自由分配单元的起始位置并不是 MemoryBlock结构体"后面的"第1个地址位置,而是MemoryBlock结构体"内部"的最后一个字节aData(也可能不是最后一个,因为 考虑到字节对齐的问题),即分配单元实际上往前面错了一位。又因为MemoryBlock结构体后面的空间刚好是分配单元的整数倍,这样依次错位下去,内 存块的最后一个字节实际没有被利用。这么做的一个原因也是考虑到不同平台的移植问题,因为不同平台的对齐方式可能不尽相同。即当申请 MemoryBlock大小内存时,可能会返回比其所有成员大小总和还要大一些的内存。最后的几个字节是为了"补齐",而使得aData成为第1个分配单 元的起始位置,这样在对齐方式不同的各种平台上都可以工作。

6.2.3 细节剖析

有了上述的总体印象后,本节来仔细剖析其实现细节。

(1)MemoryPool的构造如下:


MemoryPool::MemoryPool( USHORT _nUnitSize,
USHORT _nInitSize, USHORT _nGrowSize )
{
pBlock = NULL; ①
nInitSize = _nInitSize; ②
nGrowSize = _nGrowSize; ③

if ( _nUnitSize > 4 )
nUnitSize = (_nUnitSize + (MEMPOOL_ALIGNMENT-1)) & ~(MEMPOOL_ALIGNMENT-1); ④
else if ( _nUnitSize <= 2 )
nUnitSize = 2; ⑤
else
nUnitSize = 4;
}

从①处可以看出,MemoryPool创建时,并没有立刻创建真正用来满足内存申请的内存块,即内存块链表刚开始时为空。

②处和③处分别设置"第1次创建的内存块所包含的分配单元的个数",及"随后创建的内存块所包含的分配单元的个数",这两个值在MemoryPool创建时通过参数指定,其后在该MemoryPool对象生命周期中一直不变。

后 面的代码用来设置nUnitSize,这个值参考传入的_nUnitSize参数。但是还需要考虑两个因素。如前所述,每个分配单元在自由状态时,其头两 个字节用来存放"其下一个自由分配单元的编号"。即每个分配单元"最少"有"两个字节",这就是⑤处赋值的原因。④处是将大于4个字节的大小 _nUnitSize往上"取整到"大于_nUnitSize的最小的MEMPOOL_ ALIGNMENT的倍数(前提是MEMPOOL_ALIGNMENT为2的倍数)。如_nUnitSize为11 时,MEMPOOL_ALIGNMENT为8,nUnitSize为16;MEMPOOL_ALIGNMENT为4,nUnitSize为 12;MEMPOOL_ALIGNMENT为2,nUnitSize为12,依次类推。

(2)当向MemoryPool提出内存请求时:


void* MemoryPool::Alloc()
{
if ( !pBlock ) ①
{
……
}

MemoryBlock* pMyBlock = pBlock;
while (pMyBlock && !pMyBlock->nFree )②
pMyBlock = pMyBlock->pNext;

if ( pMyBlock ) ③
{
char* pFree = pMyBlock->aData+(pMyBlock->nFirst*nUnitSize);
pMyBlock->nFirst = *((USHORT*)pFree);

pMyBlock->nFree--;
return (void*)pFree;
}
else ④
{
if ( !nGrowSize )
return NULL;

pMyBlock = new(nGrowSize, nUnitSize) FixedMemBlock(nGrowSize, nUnitSize);
if ( !pMyBlock )
return NULL;

pMyBlock->pNext = pBlock;
pBlock = pMyBlock;

return (void*)(pMyBlock->aData);
}

}

MemoryPool满足内存请求的步骤主要由四步组成。

① 处首先判断内存池当前内存块链表是否为空,如果为空,则意味着这是第1次内存申请请求。这时,从进程堆中申请一个分配单元个数为nInitSize的内存 块,并初始化该内存块(主要初始化MemoryBlock结构体成员,以及创建初始的自由分配单元链表,下面会详细分析其代码)。如果该内存块申请成功, 并初始化完毕,返回第1个分配单元给调用函数。第1个分配单元以MemoryBlock结构体内的最后一个字节为起始地址。

②处的作用是当内存池中已有内存块(即内存块链表不为空)时遍历该内存块链表,寻找还有"自由分配单元"的内存块。

③ 处检查如果找到还有自由分配单元的内存块,则"定位"到该内存块现在可以用的自由分配单元处。"定位"以MemoryBlock结构体内的最后一个字节位 置aData为起始位置,以MemoryPool的nUnitSize为步长来进行。找到后,需要修改MemoryBlock的nFree信息(剩下来的 自由分配单元比原来减少了一个),以及修改此内存块的自由存储单元链表的信息。在找到的内存块中,pMyBlock->nFirst为该内存块中自 由存储单元链表的表头,其下一个自由存储单元的编号存放在pMyBlock->nFirst指示的自由存储单元(亦即刚才定位到的自由存储单元)的 头两个字节。通过刚才定位到的位置,取其头两个字节的值,赋给pMyBlock->nFirst,这就是此内存块的自由存储单元链表的新的表头,即 下一次分配出去的自由分配单元的编号(如果nFree大于零的话)。修改维护信息后,就可以将刚才定位到的自由分配单元的地址返回给此次申请的调用函数。 注意,因为这个分配单元已经被分配,而内存块无须维护已分配的分配单元,因此该分配单元的头两个字节的信息已经没有用处。换个角度看,这个自由分配单元返 回给调用函数后,调用函数如何处置这块内存,内存池无从知晓,也无须知晓。此分配单元在返回给调用函数时,其内容对于调用函数来说是无意义的。因此几乎可 以肯定调用函数在用这个单元的内存时会覆盖其原来的内容,即头两个字节的内容也会被抹去。因此每个存储单元并没有因为需要链接而引入多余的维护信息,而是 直接利用单元内的头两个字节,当其分配后,头两个字节也可以被调用函数利用。而在自由状态时,则用来存放维护信息,即下一个自由分配单元的编号,这是一个 有效利用内存的好例子。

④处表示在②处遍历时,没有找到还有自由分配单元的内存块,这时,需要重新向进程堆申 请一个内存块。因为不是第一次申请内存块,所以申请的内存块包含的分配单元个数为nGrowSize,而不再是nInitSize。与①处相同,先做这个 新申请内存块的初始化工作,然后将此内存块插入MemoryPool的内存块链表的头部,再将此内存块的第1个分配单元返回给调用函数。将此新内存块插入 内存块链表的头部的原因是该内存块还有很多可供分配的自由分配单元(除非nGrowSize等于1,这应该不太可能。因为内存池的含义就是一次性地从进程 堆中申请一大块内存,以供后续的多次申请),放在头部可以使得在下次收到内存申请时,减少②处对内存块的遍历时间。

可以用图6-2的MemoryPool来展示MemoryPool::Alloc的过程。图6-3是某个时刻MemoryPool的内部状态。


图6-3 某个时刻MemoryPool的内部状态
图6-3  某个时刻memorypool的内部状态

因 为MemoryPool的内存块链表不为空,因此会遍历其内存块链表。又因为第1个内存块里有自由的分配单元,所以会从第1个内存块中分配。检查 nFirst,其值为m,这时pBlock->aData+(pBlock->nFirst*nUnitSize)定位到编号为m的自由分配 单元的起始位置(用pFree表示)。在返回pFree之前,需要修改此内存块的维护信息。首先将nFree递减1,然后取得pFree处开始的头两个字 节的值(需要说明的是,这里aData处值为k。其实不是这一个字节。而是以aData和紧跟其后的另外一个字节合在一起构成的一个USHORT的值,不 可误会)。发现为k,这时修改pBlock的nFirst为k。然后,返回pFree。此时MemoryPool的结构如图6-4所示。


图6-4 MemoryPool的结构
图6-4  memorypool的结构

可以看到,原来的第1个可供分配的单元(m编号处)已经显示为被分配的状态。而pBlock的nFirst已经指向原来m单元下一个自由分配单元的编号,即k。

(3)MemoryPool回收内存时:


void MemoryPool::Free( void* pFree )
{
……

MemoryBlock* pMyBlock = pBlock;

while ( ((ULONG)pMyBlock->aData > (ULONG)pFree) ||
((ULONG)pFree >= ((ULONG)pMyBlock->aData + pMyBlock->nSize)) )①
{
……
}

pMyBlock->nFree++; ②
*((USHORT*)pFree) = pMyBlock->nFirst; ③
pMyBlock->nFirst = (USHORT)(((ULONG)pFree-(ULONG)(pBlock->aData)) / nUnitSize);④

if (pMyBlock->nFree*nUnitSize == pMyBlock->nSize )⑤
{
……
}
else
{
……
}
}

如前所述,回收分配单元时,可能会将整个内存块返回给进程堆,也可能将被回收分配单元所属的内存块移至内存池的内存块链表的头部。这两个操作都需要修改链表结构。这时需要知道该内存块在链表中前一个位置的内存块。

①处遍历内存池的内存块链表,确定该待回收分配单元(pFree)落在哪一个内存块的指针范围内,通过比较指针值来确定。

运 行到②处,pMyBlock即找到的包含pFree所指向的待回收分配单元的内存块(当然,这时应该还需要检查pMyBlock为NULL时的情形,即 pFree不属于此内存池的范围,因此不能返回给此内存池,读者可以自行加上)。这时将pMyBlock的nFree递增1,表示此内存块的自由分配单元 多了一个。

③处用来修改该内存块的自由分配单元链表的信息,它将这个待回收分配单元的头两个字节的值指向该内存块原来的第一个可分配的自由分配单元的编号。

④处将pMyBlock的nFirst值改变为指向这个待回收分配单元的编号,其编号通过计算此单元的起始位置相对pMyBlock的aData位置的差值,然后除以步长(nUnitSize)得到。

实 质上,③和④两步的作用就是将此待回收分配单元"真正回收"。值得注意的是,这两步实际上是使得此回收单元成为此内存块的下一个可分配的自由分配单元,即 将它放在了自由分配单元链表的头部。注意,其内存地址并没有发生改变。实际上,一个分配单元的内存地址无论是在分配后,还是处于自由状态时,一直都不会变 化。变化的只是其状态(已分配/自由),以及当其处于自由状态时在自由分配单元链表中的位置。

⑤处检查当回收完毕后,包含此回收单元的内存块的所有单元是否都处于自由状态,且此内存是否处于内存块链表的头部。如果是,将此内存块整个的返回给进程堆,同时修改内存块链表结构。

注 意,这里在判断一个内存块的所有单元是否都处于自由状态时,并没有遍历其所有单元,而是判断nFree乘以nUnitSize是否等于nSize。 nSize是内存块中所有分配单元的大小,而不包括头部MemoryBlock结构体的大小。这里可以看到其用意,即用来快速检查某个内存块中所有分配单 元是否全部处于自由状态。因为只需结合nFree和nUnitSize来计算得出结论,而无须遍历和计算所有自由状态的分配单元的个数。

另 外还需注意的是,这里并不能比较nFree与nInitSize或nGrowSize的大小来判断某个内存块中所有分配单元都为自由状态,这是因为第1次 分配的内存块(分配单元个数为nInitSize)可能被移到链表的后面,甚至可能在移到链表后面后,因为某个时间其所有单元都处于自由状态而被整个返回 给进程堆。即在回收分配单元时,无法判定某个内存块中的分配单元个数到底是nInitSize还是nGrowSize,也就无法通过比较nFree与 nInitSize或nGrowSize的大小来判断一个内存块的所有分配单元是否都为自由状态。

以上面分配后的内存池状态作为例子,假设这时第2个内存块中的最后一个单元需要回收(已被分配,假设其编号为m,pFree指针指向它),如图6-5所示。

不 难发现,这时nFirst的值由原来的0变为m。即此内存块下一个被分配的单元是m编号的单元,而不是0编号的单元(最先分配的是最新回收的单元,从这一 点看,这个过程与栈的原理类似,即先进后出。只不过这里的"进"意味着"回收",而"出"则意味着"分配")。相应地,m的"下一个自由单元"标记为0, 即内存块原来的"下一个将被分配出去的单元",这也表明最近回收的分配单元被插到了内存块的"自由分配单元链表"的头部。当然,nFree递增1。


图6-5 分配后的内存池状态
图6-5  分配后的内存池状态

处理至⑥处之前,其状态如图6-6所示。


图6-6 处理至⑥处之前的内存池状态
图6-6  处理至⑥处之前的内存池状态

这 里需要注意的是,虽然pFree被"回收",但是pFree仍然指向m编号的单元,这个单元在回收过程中,其头两个字节被覆写,但其他部分的内容并没有改 变。而且从整个进程的内存使用角度来看,这个m编号的单元的状态仍然是"有效的"。因为这里的"回收"只是回收给了内存池,而并没有回收给进程堆,因此程 序仍然可以通过pFree访问此单元。但是这是一个很危险的操作,因为首先该单元在回收过程中头两个字节已被覆写,并且该单元可能很快就会被内存池重新分 配。因此回收后通过pFree指针对这个单元的访问都是错误的,读操作会读到错误的数据,写操作则可能会破坏程序中其他地方的数据,因此需要格外小心。

接着,需要判断该内存块的内部使用情况,及其在内存块链表中的位置。如果该内存块中省略号"……"所表示的其他部分中还有被分配的单元,即nFree乘以nUnitSize不等于nSize。因为此内存块不在链表头,因此还需要将其移到链表头部,如图6-7所示。


图6-7 因回收引起的MemoryBlock移动
图6-7  因回收引起的memoryblock移动

如果该内存块中省略号"……"表示的其他部分中全部都是自由分配单元,即nFree乘以nUnitSize等于nSize。因为此内存块不在链表头,所以此时需要将此内存块整个回收给进程堆,回收后内存池的结构如图6-8所示。


图6-8 回收后内存池的结构
图6-8  回收后内存池的结构

一个内存块在申请后会初始化,主要是为了建立最初的自由分配单元链表,下面是其详细代码:


MemoryBlock::MemoryBlock (USHORT nTypes, USHORT nUnitSize)
: nSize (nTypes * nUnitSize),
nFree (nTypes - 1), ④
nFirst (1), ⑤
pNext (0)
{
char * pData = aData; ①
for (USHORT i = 1; i < nTypes; i++) ②
{
*reinterpret_cast<USHORT*>(pData) = i; ③
pData += nUnitSize;
}
}

这里可以看到,①处pData的初值是 aData,即0编号单元。但是②处的循环中i却是从1开始,然后在循环内部的③处将pData的头两个字节值置为i。即0号单元的头两个字节值为1,1 号单元的头两个字节值为2,一直到(nTypes-2)号单元的头两个字节值为(nTypes-1)。这意味着内存块初始时,其自由分配单元链表是从0号 开始。依次串联,一直到倒数第2个单元指向最后一个单元。

还需要注意的是,在其初始化列表中,nFree初始 化为nTypes-1(而不是nTypes),nFirst初始化为1(而不是0)。这是因为第1个单元,即0编号单元构造完毕后,立刻会被分配。另外注 意到最后一个单元初始并没有设置头两个字节的值,因为该单元初始在本内存块中并没有下一个自由分配单元。但是从上面例子中可以看到,当最后一个单元被分配 并回收后,其头两个字节会被设置。

图6-9所示为一个内存块初始化后的状态。


图6-9 一个内存块初始化后的状态
图6-9  一个内存块初始化后的状态

当内存池析构时,需要将内存池的所有内存块返回给进程堆:


MemoryPool::~MemoryPool()
{
MemoryBlock* pMyBlock = pBlock;
while ( pMyBlock )
{
……
}
}

6.2.4 使用方法

分 析内存池的内部原理后,本节说明如何使用它。从上面的分析可以看到,该内存池主要有两个对外接口函数,即Alloc和Free。Alloc返回所申请的分 配单元(固定大小内存),Free则回收传入的指针代表的分配单元的内存给内存池。分配的信息则通过MemoryPool的构造函数指定,包括分配单元大 小、内存池第1次申请的内存块中所含分配单元的个数,以及内存池后续申请的内存块所含分配单元的个数等。

综上 所述,当需要提高某些关键类对象的申请/回收效率时,可以考虑将该类所有生成对象所需的空间都从某个这样的内存池中开辟。在销毁对象时,只需要返回给该内 存池。"一个类的所有对象都分配在同一个内存池对象中"这一需求很自然的设计方法就是为这样的类声明一个静态内存池对象,同时为了让其所有对象都从这个内 存池中开辟内存,而不是缺省的从进程堆中获得,需要为该类重载一个new运算符。因为相应地,回收也是面向内存池,而不是进程的缺省堆,还需要重载一个 delete运算符。在new运算符中用内存池的Alloc函数满足所有该类对象的内存请求,而销毁某对象则可以通过在delete运算符中调用内存池的 Free完成。

6.2.5 性能比较

为 了测试利用内存池后的效果,通过一个很小的测试程序可以发现采用内存池机制后耗时为297 ms。而没有采用内存池机制则耗时625 ms,速度提高了52.48%。速度提高的原因可以归结为几点,其一,除了偶尔的内存申请和销毁会导致从进程堆中分配和销毁内存块外,绝大多数的内存申请 和销毁都由内存池在已经申请到的内存块中进行,而没有直接与进程堆打交道,而直接与进程堆打交道是很耗时的操作;其二,这是单线程环境的内存池,可以看到 内存池的Alloc和Free操作中并没有加线程保护措施。因此如果类A用到该内存池,则所有类A对象的创建和销毁都必须发生在同一个线程中。但如果类A 用到内存池,类B也用到内存池,那么类A的使用线程可以不必与类B的使用线程是同一个线程。

另外,在第1章中已经讨论过,因为内存池技术使得同类型的对象分布在相邻的内存区域,而程序会经常对同一类型的对象进行遍历操作。因此在程序运行过程中发生的缺页应该会相应少一些,但这个一般只能在真实的复杂应用环境中进行验证。





回页首


6.3 本章小结

内 存的申请和释放对一个应用程序的整体性能影响极大,甚至在很多时候成为某个应用程序的瓶颈。消除内存申请和释放引起的瓶颈的方法往往是针对内存使用的实际 情况提供一个合适的内存池。内存池之所以能够提高性能,主要是因为它能够利用应用程序的实际内存使用场景中的某些"特性"。比如某些内存申请与释放肯定发 生在一个线程中,某种类型的对象生成和销毁与应用程序中的其他类型对象要频繁得多,等等。针对这些特性,可以为这些特殊的内存使用场景提供量身定做的内存 池。这样能够消除系统提供的缺省内存机制中,对于该实际应用场景中的不必要的操作,从而提升应用程序的整体性能。



作者简介


冯 宏华,清华大学计算机科学与技术系硕士。IBM 中国开发中心高级软件工程师。 2003 年 12 月加入 IBM 中国开发中心,主要从事 IBM 产品的开发、性能优化等工作。兴趣包括 C/C++ 应用程序性能调优,Windows 应用程序开发,Web 应用程序开发等。



徐 莹,山东大学计算机科学与技术系硕士。2003 年 4 月加入 IBM 中国开发中心,现任 IBM 中国开发中心开发经理,一直从事IBM软件产品在多个操作系统平台上的开发工作。曾参与 IBM 产品在 Windows 和 Linux 平台上的性能优化工作,对 C/C++ 编程语言和跨平台的大型软件系统的开发有较丰富的经验。



程 远,北京大学计算机科学与技术系硕士。IBM 中国开发中心高级软件工程师。2003 年加入 IBM 中国开发中心,主要从事IBM Productivity Tools 产品的开发、性能优化等工作。兴趣包括 C/C++ 编程语言,软件性能工程,Windows/Linux 平台性能测试优化工具等。



汪 磊,北京航空航天大学计算机科学与技术系硕士,目前是 IBM 中国软件开发中心高级软件工程师。从 2002 年 12 月加入 IBM 中国开发中心至今一直从事旨在提高企业生产效率的应用软件开发。兴趣包括 C\C++ 应用程序的性能调优,Java 应用程序的性能调优。



posted @ 2008-11-17 16:37 micheal's tech 阅读(388) | 评论 (0)编辑 收藏

The "Double-Checked Locking is Broken" Declaration

Signed by : David Bacon (IBM Research) Joshua Bloch (Javasoft), Jeff Bogda, Cliff Click (Hotspot JVM project), Paul Haahr, Doug Lea, Tom May, Jan-Willem Maessen, Jeremy Manson, John D. Mitchell (jGuru) Kelvin Nilsen, Bill Pugh, Emin Gun Sirer

Double-Checked Locking is widely cited and used as an efficient method for implementing lazy initialization in a multithreaded environment.

Unfortunately, it will not work reliably in a platform independent way when implemented in Java, without additional synchronization. When implemented in other languages, such as C++, it depends on the memory model of the processor, the reorderings performed by the compiler and the interaction between the compiler and the synchronization library. Since none of these are specified in a language such as C++, little can be said about the situations in which it will work. Explicit memory barriers can be used to make it work in C++, but these barriers are not available in Java.

To first explain the desired behavior, consider the following code:

// Single threaded version
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// other functions and members...
}

If this code was used in a multithreaded context, many things could go wrong. Most obviously, two or more Helper objects could be allocated. (We'll bring up other problems later). The fix to this is simply to synchronize the getHelper() method:

// Correct multithreaded version
class Foo {
private Helper helper = null;
public synchronized Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// other functions and members...
}

The code above performs synchronization every time getHelper() is called. The double-checked locking idiom tries to avoid synchronization after the helper is allocated:

// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other functions and members...
}

Unfortunately, that code just does not work in the presence of either optimizing compilers or shared memory multiprocessors.

It doesn't work

There are lots of reasons it doesn't work. The first couple of reasons we'll describe are more obvious. After understanding those, you may be tempted to try to devise a way to "fix" the double-checked locking idiom. Your fixes will not work: there are more subtle reasons why your fix won't work. Understand those reasons, come up with a better fix, and it still won't work, because there are even more subtle reasons.

Lots of very smart people have spent lots of time looking at this. There is no way to make it work without requiring each thread that accesses the helper object to perform synchronization.

The first reason it doesn't work

The most obvious reason it doesn't work it that the writes that initialize the Helper object and the write to the helper field can be done or perceived out of order. Thus, a thread which invokes getHelper() could see a non-null reference to a helper object, but see the default values for fields of the helper object, rather than the values set in the constructor.

If the compiler inlines the call to the constructor, then the writes that initialize the object and the write to the helper field can be freely reordered if the compiler can prove that the constructor cannot throw an exception or perform synchronization.

Even if the compiler does not reorder those writes, on a multiprocessor the processor or the memory system may reorder those writes, as perceived by a thread running on another processor.

Doug Lea has written a more detailed description of compiler-based reorderings.

A test case showing that it doesn't work

Paul Jakubik found an example of a use of double-checked locking that did not work correctly. A slightly cleaned up version of that code is available here.

When run on a system using the Symantec JIT, it doesn't work. In particular, the Symantec JIT compiles

singletons[i].reference = new Singleton();

to the following (note that the Symantec JIT using a handle-based object allocation system).

0206106A   mov         eax,0F97E78h
0206106F call 01F6B210 ; allocate space for
; Singleton, return result in eax
02061074 mov dword ptr [ebp],eax ; EBP is &singletons[i].reference
; store the unconstructed object here.
02061077 mov ecx,dword ptr [eax] ; dereference the handle to
; get the raw pointer
02061079 mov dword ptr [ecx],100h ; Next 4 lines are
0206107F mov dword ptr [ecx+4],200h ; Singleton's inlined constructor
02061086 mov dword ptr [ecx+8],400h
0206108D mov dword ptr [ecx+0Ch],0F84030h

As you can see, the assignment to singletons[i].reference is performed before the constructor for Singleton is called. This is completely legal under the existing Java memory model, and also legal in C and C++ (since neither of them have a memory model).

A fix that doesn't work

Given the explanation above, a number of people have suggested the following code:

// (Still) Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
Helper h;
synchronized(this) {
h = helper;
if (h == null)
synchronized (this) {
h = new Helper();
} // release inner synchronization lock
helper = h;
}
}
return helper;
}
// other functions and members...
}

This code puts construction of the Helper object inside an inner synchronized block. The intuitive idea here is that there should be a memory barrier at the point where synchronization is released, and that should prevent the reordering of the initialization of the Helper object and the assignment to the field helper.

Unfortunately, that intuition is absolutely wrong. The rules for synchronization don't work that way. The rule for a monitorexit (i.e., releasing synchronization) is that actions before the monitorexit must be performed before the monitor is released. However, there is no rule which says that actions after the monitorexit may not be done before the monitor is released. It is perfectly reasonable and legal for the compiler to move the assignment helper = h; inside the synchronized block, in which case we are back where we were previously. Many processors offer instructions that perform this kind of one-way memory barrier. Changing the semantics to require releasing a lock to be a full memory barrier would have performance penalties.

More fixes that don't work

There is something you can do to force the writer to perform a full bidirectional memory barrier. This is gross, inefficient, and is almost guaranteed not to work once the Java Memory Model is revised. Do not use this. In the interests of science, I've put a description of this technique on a separate page. Do not use it.

However , even with a full memory barrier being performed by the thread that initializes the helper object, it still doesn't work.

The problem is that on some systems, the thread which sees a non-null value for the helper field also needs to perform memory barriers.

Why? Because processors have their own locally cached copies of memory. On some processors, unless the processor performs a cache coherence instruction (e.g., a memory barrier), reads can be performed out of stale locally cached copies, even if other processors used memory barriers to force their writes into global memory.

I've created a separate web page with a discussion of how this can actually happen on an Alpha processor.

Is it worth the trouble?

For most applications, the cost of simply making the getHelper() method synchronized is not high. You should only consider this kind of detailed optimizations if you know that it is causing a substantial overhead for an application.

Very often, more high level cleverness, such as using the builtin mergesort rather than handling exchange sort (see the SPECJVM DB benchmark) will have much more impact.

Making it work for static singletons

If the singleton you are creating is static (i.e., there will only be one Helper created), as opposed to a property of another object (e.g., there will be one Helper for each Foo object, there is a simple and elegant solution.

Just define the singleton as a static field in a separate class. The semantics of Java guarantee that the field will not be initialized until the field is referenced, and that any thread which accesses the field will see all of the writes resulting from initializing that field.

class HelperSingleton {
static Helper singleton = new Helper();
}

It will work for 32-bit primitive values

Although the double-checked locking idiom cannot be used for references to objects, it can work for 32-bit primitive values (e.g., int's or float's). Note that it does not work for long's or double's, since unsynchronized reads/writes of 64-bit primitives are not guaranteed to be atomic.

// Correct Double-Checked Locking for 32-bit primitives
class Foo {
private int cachedHashCode = 0;
public int hashCode() {
int h = cachedHashCode;
if (h == 0)
synchronized(this) {
if (cachedHashCode != 0) return cachedHashCode;
h = computeHashCode();
cachedHashCode = h;
}
return h;
}
// other functions and members...
}

In fact, assuming that the computeHashCode function always returned the same result and had no side effects (i.e., idempotent), you could even get rid of all of the synchronization.

// Lazy initialization 32-bit primitives
// Thread-safe if computeHashCode is idempotent
class Foo {
private int cachedHashCode = 0;
public int hashCode() {
int h = cachedHashCode;
if (h == 0) {
h = computeHashCode();
cachedHashCode = h;
}
return h;
}
// other functions and members...
}

Making it work with explicit memory barriers

It is possible to make the double checked locking pattern work if you have explicit memory barrier instructions. For example, if you are programming in C++, you can use the code from Doug Schmidt et al.'s book:

// C++ implementation with explicit memory barriers
// Should work on any platform, including DEC Alphas
// From "Patterns for Concurrent and Distributed Objects",
// by Doug Schmidt
template <class TYPE, class LOCK> TYPE *
Singleton<TYPE, LOCK>::instance (void) {
// First check
TYPE* tmp = instance_;
// Insert the CPU-specific memory barrier instruction
// to synchronize the cache lines on multi-processor.
asm ("memoryBarrier");
if (tmp == 0) {
// Ensure serialization (guard
// constructor acquires lock_).
Guard<LOCK> guard (lock_);
// Double check.
tmp = instance_;
if (tmp == 0) {
tmp = new TYPE;
// Insert the CPU-specific memory barrier instruction
// to synchronize the cache lines on multi-processor.
asm ("memoryBarrier");
instance_ = tmp;
}
return tmp;
}

Fixing Double-Checked Locking using Thread Local Storage

Alexander Terekhov (TEREKHOV@de.ibm.com) came up clever suggestion for implementing double checked locking using thread local storage. Each thread keeps a thread local flag to determine whether that thread has done the required synchronization.
  class Foo {
/** If perThreadInstance.get() returns a non-null value, this thread
has done synchronization needed to see initialization
of helper */
private final ThreadLocal perThreadInstance = new ThreadLocal();
private Helper helper = null;
public Helper getHelper() {
if (perThreadInstance.get() == null) createHelper();
return helper;
}
private final void createHelper() {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
// Any non-null value would do as the argument here
perThreadInstance.set(perThreadInstance);
}
}

The performance of this technique depends quite a bit on which JDK implementation you have. In Sun's 1.2 implementation, ThreadLocal's were very slow. They are significantly faster in 1.3, and are expected to be faster still in 1.4. Doug Lea analyzed the performance of some techniques for implementing lazy initialization.

Under the new Java Memory Model

As of JDK5, there is a new Java Memory Model and Thread specification.

Fixing Double-Checked Locking using Volatile

JDK5 and later extends the semantics for volatile so that the system will not allow a write of a volatile to be reordered with respect to any previous read or write, and a read of a volatile cannot be reordered with respect to any following read or write. See this entry in Jeremy Manson's blog for more details.

With this change, the Double-Checked Locking idiom can be made to work by declaring the helper field to be volatile. This does not work under JDK4 and earlier.

// Works with acquire/release semantics for volatile
// Broken under current semantics for volatile
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
}

Double-Checked Locking Immutable Objects

If Helper is an immutable object, such that all of the fields of Helper are final, then double-checked locking will work without having to use volatile fields. The idea is that a reference to an immutable object (such as a String or an Integer) should behave in much the same way as an int or float; reading and writing references to immutable objects are atomic.

Descriptions of double-check idiom


posted @ 2008-10-23 14:25 micheal's tech 阅读(605) | 评论 (0)编辑 收藏

0 引言

0.1 目的

       本文档给出设计模式之——AbstractFactory模式的简化诠释,并给出其C++实现。

0.2 说明

Project

Design Pattern Explanation(By K_Eckel)

Authorization

Free Distributed but Ownership Reserved

Date

Test Bed

MS Visual C++ 6.0

0.3 参考

       在本文档的写作中,参考了以下的资源,在此列出表示感谢:

u       书籍

[GoF 2000]:GoF,Design Patterns-Elements of Reusable Object-Oriented Software

Addison-Wesley 2000/9.

        [Martine 2003]:Robert C.Martine, Agile Software Development Principles, Patterns, and Practices, Pearson Education, 2003.

0.4 联系作者

Author

K_Eckel

State

Candidate for Master’s Degree School of

E_mail

frwei@whu.edu.cn  

2 AbstractFactory模式

2.1 问题

       假设我们要开发一款游戏,当然为了吸引更多的人玩,游戏难度不能太大(让大家都没有信心了,估计游戏也就没有前途了),但是也不能太简单(没有挑战性也不符合玩家的心理)。于是我们就可以采用这样一种处理策略:为游戏设立等级,初级、中级、高级甚至有BT级。假设也是过关的游戏,每个关卡都有一些怪物(monster)守着,玩家要把这些怪 物干掉才可以过关。作为开发者,我们就不得不创建怪物的类,然后初级怪物、中级怪物等都继承自怪物类(当然不同种类的则需要另创建类,但是模式相同)。在 每个关卡,我们都要创建怪物的实例,例如初级就创建初级怪物(有很多种类)、中级创建中级怪物等。可以想象在这个系统中,将会有成千上万的怪物实例要创 建,问题是还要保证创建的时候不会出错:初级不能创建BT级的怪物(玩家就郁闷了,玩家一郁闷,游戏也就挂挂了),反之也不可以。

       AbstractFactory模式就是用来解决这类问题的:要创建一组相关或者相互依赖的对象。

2.2 模式选择

       AbstractFactory模式典型的结构图为:


2-1AbstractFactoryPattern结构图

       AbstractFactory模式关键就是将这一组对象的创建封装到一个用于创建对象的类(ConcreteFactory)中,维护这样一个创建类总比维护n多相关对象的创建过程要简单的多。

2.3 实现

       AbstractFactory模式的实现比较简单,这里为了方便初学者的学习和参考,将给出完整的实现代码(所有代码采用C++实现,并在VC 6.0下测试运行)。

代码片断1Product.h
//Product.h

#ifndef _PRODUCT_H_
#define _PRODUCT_H_

class AbstractProductA
{
public:
 virtual ~AbstractProductA();

protected:
 AbstractProductA();

private:

};

class AbstractProductB
{
public:
 virtual ~AbstractProductB();

protected:
 AbstractProductB();

private:

};

class ProductA1:public AbstractProductA
{
public:
 ProductA1();

 ~ProductA1();

protected:

private:

};

class ProductA2:public AbstractProductA
{
public:
 ProductA2();

 ~ProductA2();

protected:

private:

};

class ProductB1:public AbstractProductB
{
public:
 ProductB1();

 ~ProductB1();

protected:

private:

};

class ProductB2:public AbstractProductB
{
public:
 ProductB2();

 ~ProductB2();

protected:

private:

};

#endif //~_PRODUCT_H_

代码片断2Product.cpp
//Product.cpp

#include "Product.h"

#include <iostream>
using namespace std;

AbstractProductA::AbstractProductA()
{

}

AbstractProductA::~AbstractProductA()
{

}

AbstractProductB::AbstractProductB()
{

}

AbstractProductB::~AbstractProductB()
{

}

ProductA1::ProductA1()
{
 cout<<"ProductA1..."<<endl;
}

ProductA1::~ProductA1()
{

}

ProductA2::ProductA2()
{
 cout<<"ProductA2..."<<endl;
}

ProductA2::~ProductA2()
{

}

ProductB1::ProductB1()
{
 cout<<"ProductB1..."<<endl;
}

ProductB1::~ProductB1()
{

}

ProductB2::ProductB2()
{
 cout<<"ProductB2..."<<endl;
}

ProductB2::~ProductB2()
{

}

代码片断3AbstractFactory.h
//AbstractFactory.h

#ifndef _ABSTRACTFACTORY_H_
#define _ABSTRACTFACTORY_H_

class AbstractProductA;
class AbstractProductB;

class AbstractFactory
{
public:
 virtual ~AbstractFactory();

 virtual AbstractProductA* CreateProductA() = 0;

 virtual AbstractProductB* CreateProductB() = 0;

protected:
 AbstractFactory();

private:

};

class ConcreteFactory1:public AbstractFactory
{
public:
 ConcreteFactory1();

 ~ConcreteFactory1();

 AbstractProductA* CreateProductA();

 AbstractProductB* CreateProductB();

protected:

private:

};

class ConcreteFactory2:public AbstractFactory
{
public:
 ConcreteFactory2();

 ~ConcreteFactory2();

 AbstractProductA* CreateProductA();

 AbstractProductB* CreateProductB();

protected:

private:

};
#endif //~_ABSTRACTFACTORY_H_

代码片断4AbstractFactory.cpp
//AbstractFactory.cpp

#include "AbstractFactory.h"
#include "Product.h"

#include <iostream>
using namespace std;

AbstractFactory::AbstractFactory()
{

}

AbstractFactory::~AbstractFactory()
{

}

ConcreteFactory1::ConcreteFactory1()
{

}

ConcreteFactory1::~ConcreteFactory1()
{

}

AbstractProductA* ConcreteFactory1::CreateProductA()
{
 return new ProductA1();
}

AbstractProductB* ConcreteFactory1::CreateProductB()
{
 return new ProductB1();
}

ConcreteFactory2::ConcreteFactory2()
{

}

ConcreteFactory2::~ConcreteFactory2()
{

}

AbstractProductA* ConcreteFactory2::CreateProductA()
{
 return new ProductA2();
}

AbstractProductB* ConcreteFactory2::CreateProductB()
{
 return new ProductB2();
}

代码片断5main.cpp
//main.cpp

#include "AbstractFactory.h"

#include <iostream>
using namespace std;

int main(int argc,char* argv[])
{
 AbstractFactory* cf1 = new ConcreteFactory1();

 cf1->CreateProductA();
 cf1->CreateProductB();

 AbstractFactory* cf2 = new ConcreteFactory2();
 cf2->CreateProductA();
 cf2->CreateProductB();

 return 0;
}

       AbstractFactory模式的实现代码很简单,在测试程序中可以看到,当我们要创建一组对象(ProductA1,ProductA2)的时候我们只用维护一个创建对象(ConcreteFactory1),大大简化了维护的成本和工作。

2.4 讨论

       AbstractFactory模式和Factory模式的区别是初学(使用)设计模式时候的一个容易引起困惑的地方。实际上,AbstractFactory模式是为创建一组(有多类)相关或依赖的对象提供创建接口,而Factory模式正如我在相应的文档中分析的是为一类对象提供创建接口或延迟对象的创建到子类中实现。并且可以看到,AbstractFactory模式通常都是使用Factory模式实现(ConcreteFactory1)。


posted @ 2008-10-22 10:44 micheal's tech 阅读(368) | 评论 (0)编辑 收藏

0 引言

0.1 目的

       本文档给出设计模式之——Factory模式的简化诠释,并给出其C++实现。

0.2 说明

Project

Design Pattern Explanation(By K_Eckel)

Authorization

Free Distributed but Ownership Reserved

Date

Test Bed

MS Visual C++ 6.0

0.3 参考

       在本文档的写作中,参考了以下的资源,在此列出表示感谢:

u       书籍

[GoF 2000]:GoF,Design Patterns-Elements of Reusable Object-Oriented Software Addison-Wesley 2000/9.

        [Martine 2003]:Robert C.Martine, Agile Software Development Principles, Patterns, and Practices, Pearson Education, 2003.

u       网页

0.4 联系作者

Author

K_Eckel

State

Candidate for Master’s Degree School of

E_mail

frwei@whu.edu.cn  

 

2 Factory模式

2.1 问题

       在面向对象系统设计中经常可以遇到以下的两类问题:

       1)为了提高内聚(Cohesion)和松耦合(Coupling),我们经常会抽象出一些类的公共接口以形成抽象基类或者接口。这样我们可以通过声明一个指向基类的指针来指向实际的子类实现,达到了多态的目的。这里很容易出现的一个问题n多的子类继承自抽象基类,我们不得不在每次要用到子类的地方就编写诸如new ×××;的代码。这里带来两个问题1)客户程序员必须知道实际子类的名称(当系统复杂后,命名将是一个很不好处理的问题,为了处理可能的名字冲突,有的命名可能并不是具有很好的可读性和可记忆性,就姑且不论不同程序员千奇百怪的个人偏好了。),2)程序的扩展性和维护变得越来越困难。

       2)还有一种情况就是在父类中并不知道具体要实例化哪一个具体的子类。这里的意思为:假设我们在类A中要使用到类B,B是一个抽象父类,在A中并不知道具体要实例化那一个B的子类,但是在类A的子类D中是可以知道的。在A中我们没有办法直接使用类似于new ×××的语句,因为根本就不知道×××是什么。

       以上两个问题也就引出了Factory模式的两个最重要的功能:

       1)定义创建对象的接口,封装了对象的创建;

       2)使得具体化类的工作延迟到了子类中。

2.2 模式选择

       我们通常使用Factory模式来解决上面给出的两个问题。在第一个问题中,我们经常就是声明一个创建对象的接口,并封装了对象的创建过程。Factory这里类似于一个真正意义上的工厂(生产对象)。在第二个问题中,我们需要提供一个对象创建对象的接口,并在子类中提供其具体实现(因为只有在子类中可以决定到底实例化哪一个类)。第一中情况的Factory的结构示意图为:


1Factory模式结构示意图1

       1所以的Factory模式经常在系统开发中用到,但是这并不是Factory模式的最大威力所在(因为这可以通过其他方式解决这个问题)。Factory模式不单是提供了创建对象的接口,其最重要的是延迟了子类的实例化(第二个问题),以下是这种情况的一个Factory的结构示意图:


2Factory模式结构示意图1

       2中关键中Factory模式的应用并不是只是为了封装对象的创建,而是要把对象的创建放到子类中实现:Factory中只是提供了对象创建的接口,其实现将放在Factory的子类ConcreteFactory中进行。这是图2和图1的区别所在。

2.3 实现

 代码片断1:Product.h
//Product.h

#ifndef _PRODUCT_H_
#define _PRODUCT_H_

class Product
{
public:
virtual ~Product() =0;

protected:
Product();

private:

};

class ConcreteProduct:publicProduct
{
public:
~ConcreteProduct();

ConcreteProduct();

protected:

private:

};

#endif //~_PRODUCT_H_

代码片断2:Product.cpp
//Product.cpp

#include "Product.h"

#include<iostream>
using namespace std;

Product::Product()
{

}

Product::~Product()
{

}

ConcreteProduct::ConcreteProduct()
{
cout<<"ConcreteProduct...."<<endl;
}

ConcreteProduct::~ConcreteProduct()
{

}

代码片断3:Factory.h
//Factory.h

#ifndef _FACTORY_H_
#define _FACTORY_H_

class Product;

class Factory
{
public:
 virtual ~Factory() = 0;

 virtual Product* CreateProduct() = 0;

protected:
 Factory();

private:

};

class ConcreteFactory:public Factory
{
public:

 ~ConcreteFactory();

 ConcreteFactory();

 Product* CreateProduct();

protected:

private:

};

#endif //~_FACTORY_H_

代码片断4:Factory.cpp
//Factory.cpp

#include "Factory.h"
#include "Product.h"

#include <iostream>
using namespace std;

Factory::Factory()
{

}

Factory::~Factory()
{

}

ConcreteFactory::ConcreteFactory()
{
 cout<<"ConcreteFactory....."<<endl;
}

ConcreteFactory::~ConcreteFactory()
{

}

Product* ConcreteFactory::CreateProduct()
{
 return new ConcreteProduct();
}

代码片断5:main.cpp
//main.cpp

#include "Factory.h"
#include "Product.h"

#include <iostream>
using namespace std;

int main(int argc,char* argv[])
{
 Factory* fac = new ConcreteFactory();

 Product* p = fac->CreateProduct();

 return 0;
}


2.4 讨论

     Factory 模式在实际开发中应用非常广泛,面向对象的系统经常面临着对象创建问题:要创建的类实在是太多了。而Factory提供的创建对象的接口封装(第一个功 能),以及其将类的实例化推迟到子类(第二个功能)都部分地解决了实际问题。一个简单的例子就是笔者开开发Visual CMCS系统的语义分析过程中,由于要为文法中的每个非终结符构造一个类处理,因此这个过程中对象的创建非常多,采用Factory模式后系统可读性性和 维护都变得elegant许多。
     Factory模式也带来至少以下两个问题:
     1)如果为每一个具体的ConcreteProduct类的实例化提供一个函数体,那么我们可能不得不在系统中添加了一个方法来处理这个新建的 ConcreteProduct,这样Factory的接口永远就不肯能封闭(Close)。当然我们可以通过创建一个Factory的子类来通过多态实 现这一点,但是这也是以新建一个类作为代价的。
     2)在实现中我们可以通过参数化工厂方法,即给FactoryMethod()传递一个参数用以决定是创建具体哪一个具体的Product(实际上笔者在 Visual CMCS中也正是这样做的)。当然也可以通过模板化避免1)中的子类创建子类,其方法就是将具体Product类作为模板参数,实现起来也很简单。
     可以看出,Factory模式对于对象的创建给予开发人员提供了很好的实现策略,但是Factory模式仅仅局限于一类类(就是说Product是一类, 有一个共同的基类),如果我们要为不同类的类提供一个对象创建的接口,那就要用Abstract Factory了。


posted @ 2008-10-21 16:45 micheal's tech 阅读(624) | 评论 (0)编辑 收藏

class Decorator:public Beverage
{
    public:
        Decorator(Beverage * com);
        virtual ~Decorator();
        virtual string get_descrption();
    protected:
        Beverage * component;


};

而MilkDecorator继承了Decorator,如果component 为私有的则MilkDecorator便不能访问。

如果milkDecorator 设计成这样就不会违反了封装的原则。
基本上只有一个区别,就是protect成员能被派生类访问!而派生类对private没有特殊访问权!

posted @ 2008-10-16 17:04 micheal's tech 阅读(231) | 评论 (0)编辑 收藏

http://book.csdn.net/bookfiles/575/10057518902.shtml

虚线箭头表示“依赖关系”,依赖有“使用”的语义,比如患者与医生的关系。
实线箭头表示“带了导航行的关联关系”,从一个类到另一类。
使用实线箭头时通常会带上“多重性”的表达方式。如:一对多,一对一,多对多等等

常见的关系有:一般化关系(Generalization),关联关系(Association),聚合关系(Aggregation),合成关系(Composition),依赖关系(Dependency)。

      其中,聚合关系(Aggregation),合成关系(Composition)属于关联关系(Association)。

      一般关系表现为继承或实现关系(is a),关联关系表现为变量(has a ),依赖关系表现为函数中的参数(use a)。

      一般化关系:表示为类与类之间的继承关系,接口与接口之间的继承,类对接口的实现关系。
      表示方法: 用一个空心箭头+实线,箭头指向父类。或空心箭头+虚线,如果父类是接口。

      关联关系:类与类之间的联接,它使一个类知道另一个类的属性和方法。
      表示方法:用 实线+箭头, 箭头指向被使用的类。

      聚合关系:是关联关系的一种,是强的关联关系。聚合关系是整体和个体的关系。关联关系的两个类处于同一层次上,啊聚合关系两个类处于不同的层次,一个是整体,一个是部分。
      表示方法:空心菱形+实线+箭头,箭头指向部分。

      合成关系:是关联关系的一种,是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期,合成关系不能共享。
      表示方法:实心菱形+实线+箭头,

      依赖关系:是类与类之间的连接,表示一个类依赖于另一个类的定义。例如如果A依赖于B,则B体现为局部变量,方法的参数、或静态方法的调用。
      表示方法:虚线+箭头


图一:

uploads/200706/04_211918_1121523.jpg


此实线箭头表示, 继承, 从一个非接口类的继承.

图二:
uploads/200706/04_212112_1121525gl.jpg


那条连线表示双向关联:
看左边, Flight扮演assignedFights角色, 有0到1个Plane跟他关联(一个航班要么取消了没有飞机,要么只能对应一架飞机)
看右边, Plane扮演着assignedPlane角色, 有0到多个Flight跟他关联(一个飞机可以参与多个航班, 也可以停在仓库里面烂掉)

图三:
uploads/200706/04_213002_1121526dxgl.jpg


那条连线表示单向关联:
基本的意义跟上面的是一样的, 唯一不同的是, 右边的类对左边的类是一无所知的.

图四:
uploads/200706/04_213232_1121527rjb.jpg


那个大的包围的框叫软件包, 名字为Account, 就一些可以归类的类包装起来.

图五:
uploads/200706/04_213441_1121529xjc.gif


如此虚线的箭头表示实现一个接口.

图六:
uploads/200706/04_213626_11215210gll.jpg


水平的连线还是表示上面所说的关联, 但从关联连线中引伸出来的虚线, 这意味当Flight类的一个实例关联到 FrequentFlyer 类的一个实例时,将会产生 MileageCredit 类的一个实例.

图七:
uploads/200706/04_213911_11215211jbjh.jpg


带菱形的箭头表示基本聚合, 由上图知道, Wheel类扮演wheels角色, 聚合4个到Car对象里面去,
空心的菱形表示Wheel对象并不随Car的创建而创建,销毁而销毁.

图八:
uploads/200706/04_214248_11215212zhjh.jpg


意义和上面类似, 唯一不同的是, 实心菱形表示Department对象随Company对象的创建而创建,销毁而销毁.

图九:
uploads/200706/04_214435_11215213fs.gif


表示反射关联, 显示一个Employee类如何通过manager / manages角色与它本身相关。当一个类关联到它本身时,这并不意味着类的实例与它本身相关,而是类的一个实例与类的另一个实例相关。

posted @ 2008-10-13 14:53 micheal's tech 阅读(1761) | 评论 (0)编辑 收藏

0 引言

0.1 目的

       本文档给出设计模式之——Observer模式的简化诠释,并给出其C++实现。

0.2 说明

Project

Design Pattern Explanation(By K_Eckel)

Authorization

Free Distributed but Ownership Reserved

Date

Test Bed

MS Visual C++ 6.0

0.3 参考

       在本文档的写作中,参考了以下的资源,在此列出表示感谢:

u       书籍

[GoF 2000]:GoF,Design Patterns-Elements of Reusable Object-Oriented Software

Addison-Wesley 2000/9.

        [Martine 2003]:Robert C.Martine, Agile Software Development Principles, Patterns, and Practices, Pearson Education, 2003.

0.4 联系作者

Author

K_Eckel

State

Candidate for Master’s Degree School of

E_mail

frwei@whu.edu.cn  

2 Observer模式

2.1 问题

       Observer模式应该可以说是应用最多、影响最广的模式之一,因为Observer的一个实例Model/View/Control(MVC)结构在系统开发架构设计中有着很重要的地位和意义,MVC实现了业务逻辑和表示层的解耦。个人也认为Observer模式是软件开发过程中必须要掌握和使用的模式之一。在MFC中,Doc/View(文档视图结构)提供了实现MVC的框架结构(有一个从设计模式(Observer模式)的角度分析分析Doc/View的文章正在进一步的撰写当中,遗憾的是时间:))。在Java阵容中,Struts则提供和MFC中Doc/View结构类似的实现MVC的框架。另外Java语言本身就提供了Observer模式的实现接口,这将在讨论中给出。

       当然,MVC只是Observer模式的一个实例。Observer模式要解决的问题为:建立一个一(Subject)对多(Observer) 的依赖关系,并且做到当“一”变化的时候,依赖这个“一”的多也能够同步改变。最常见的一个例子就是:对同一组数据进行统计分析时候,我们希望能够提供多 种形式的表示(例如以表格进行统计显示、柱状图统计显示、百分比统计显示等)。这些表示都依赖于同一组数据,我们当然需要当数据改变的时候,所有的统计的 显示都能够同时改变。Observer模式就是解决了这一个问题。

2.2 模式选择

       Observer模式典型的结构图为:


2-1Observer Pattern结构图

       这里的目标Subject提供依赖于它的观察者Observer的注册(Attach)和注销(Detach)操作,并且提供了使得依赖于它的所有观察者同步的操作(Notify)。观察者Observer则提供一个Update操作,注意这里的ObserverUpdate操作并不在Observer改变了Subject目标状态的时候就对自己进行更新,这个更新操作要延迟到Subject对象发出Notify通知所有Observer进行修改(调用Update)。

2.3 实现

       Observer模式的实现有些特点,这里为了方便初学者的学习和参考,将给出完整的实现代码(所有代码采用C++实现,并在VC 6.0下测试运行)。

代码片断1Subject.h
//Subject.h

#ifndef _SUBJECT_H_
#define _SUBJECT_H_

#include <list>
#include <string>
using namespace std;

typedef string State;

class Observer;

class Subject
{
public:
 virtual ~Subject();

 virtual void Attach(Observer* obv);

 virtual void Detach(Observer* obv);

 virtual void Notify();

 virtual void SetState(const State& st) = 0;

 virtual State GetState() = 0;

protected:
 Subject();

private:
 list<Observer* >* _obvs;

};

class ConcreteSubject:public Subject
{
public:
 ConcreteSubject();

 ~ConcreteSubject();

 State GetState();

 void SetState(const State& st);

protected:

private:
 State _st;

};

#endif //~_SUBJECT_H_

代码片断2Subject.cpp
//Subject.cpp

#include "Subject.h"
#include "Observer.h"

#include <iostream>
#include <list>
using namespace std;

typedef string state;

Subject::Subject()
{
 //****在模板的使用之前一定要new,创建
 _obvs = new list<Observer*>;

}

Subject::~Subject()
{
 
}

void Subject::Attach(Observer* obv)
{
 
 _obvs->push_front(obv);
}

void Subject::Detach(Observer* obv)
{
 if (obv != NULL)
  _obvs->remove(obv);
}

void Subject::Notify()
{
 
 list<Observer*>::iterator it;

 it = _obvs->begin();

 for (;it != _obvs->end();it++)
 {
  //关于模板和iterator的用法

  (*it)->Update(this);
 }
}

ConcreteSubject::ConcreteSubject()
{
 _st = '\0';
}

ConcreteSubject::~ConcreteSubject()
{
 
}


State ConcreteSubject::GetState()
{
 return _st;
}

void ConcreteSubject::SetState(const State& st)
{
 _st = st;
}

代码片断3Observer.h
//Observer.h

#ifndef _OBSERVER_H_
#define _OBSERVER_H_

#include "Subject.h"

#include <string>
using namespace std;

typedef string State;

class Observer
{
public:
 virtual ~Observer();

 virtual void Update(Subject* sub) = 0;

 virtual void PrintInfo() = 0;

protected:
 Observer();

 State _st;

private:

};

class ConcreteObserverA:public Observer
{
public:
 virtual Subject* GetSubject();
 
 ConcreteObserverA(Subject* sub);

 virtual ~ConcreteObserverA();

 //传入Subject作为参数,这样可以让一个View属于多个的Subject
 void  Update(Subject* sub);

 void PrintInfo();

protected:

private:
 Subject* _sub;

};

class ConcreteObserverB:public Observer
{
public:
 virtual Subject* GetSubject();
 
 ConcreteObserverB(Subject* sub);

 virtual ~ConcreteObserverB();

 //传入Subject作为参数,这样可以让一个View属于多个的Subject
 void  Update(Subject* sub);

 void PrintInfo();

protected:

private:
 Subject* _sub;

};

#endif //~_OBSERVER_H_

代码片断4Observer.cpp
//Observer.cpp

#include "Observer.h"
#include "Subject.h"

#include <iostream>
#include <string>
using namespace std;

Observer::Observer()
{
 _st = '\0';

}

Observer::~Observer()
{

}


ConcreteObserverA::ConcreteObserverA(Subject* sub)
{
 _sub = sub;

 _sub->Attach(this);
}

ConcreteObserverA::~ConcreteObserverA()
{
 _sub->Detach(this);

 if (_sub != 0)
 {
  delete _sub;
 }
}

Subject* ConcreteObserverA::GetSubject()

 return _sub;
}

void ConcreteObserverA::PrintInfo()
{
 cout<<"ConcreteObserverA observer.... "<<_sub->GetState()<<endl;
}

void ConcreteObserverA::Update(Subject* sub)
{
 _st = sub->GetState();

 PrintInfo();
}

ConcreteObserverB::ConcreteObserverB(Subject* sub)
{
 _sub = sub;

 _sub->Attach(this);
}

ConcreteObserverB::~ConcreteObserverB()
{
 _sub->Detach(this);

 if (_sub != 0)
 {
  delete _sub;
 }
}

Subject* ConcreteObserverB::GetSubject()

 return _sub;
}

void ConcreteObserverB::PrintInfo()
{
 cout<<"ConcreteObserverB observer.... "<<_sub->GetState()<<endl;
}

void ConcreteObserverB::Update(Subject* sub)
{
 _st = sub->GetState();

 PrintInfo();
}

代码片断5main.cpp
//main.cpp

#include "Subject.h"
#include "Observer.h"

#include <iostream>
using namespace std;

int main(int argc,char* argv[])
{
 ConcreteSubject* sub = new ConcreteSubject();

 Observer* o1 = new ConcreteObserverA(sub);

 Observer* o2 = new ConcreteObserverB(sub);

 sub->SetState("old");

 sub->Notify();

 sub->SetState("new"); //也可以由Observer调用

 sub->Notify();

 return 0;
}

在Observer模式的实现中,Subject维护一个list作为存储其所有观察者的容器。每当调用Notify操作就遍历list中的Observer对象,并广播通知改变状态(调用Observer的Update操作)。目标的状态state可以由Subject自己改变(示例),也可以由Observer的某个操作引起state的改变(可调用Subject的SetState操作)。Notify操作可以由Subject目标主动广播(示例),也可以由Observer观察者来调用(因为Observer维护一个指向Subject的指针)。

       运行示例程序,可以看到当Subject处于状态“old”时候,依赖于它的两个观察者都显示“old”,当目标状态改变为“new”的时候,依赖于它的两个观察者也都改变为“new”。

2.4 讨论

       Observer是影响极为深远的模式之一,也是在大型系统开发过程中要用到的模式之一。除了MFC、Struts提供了MVC的实现框架,在Java语言中还提供了专门的接口实现Observer模式:通过专门的类Observable及Observer接口来实现MVC编程模式,其UML图可以表示为:

 


Java中实现MVC的UML图。

这里的Observer就是观察者,Observable则充当目标Subject的角色。

       Observer模式也称为发布-订阅(publish-subscribe),目标就是通知的发布者,观察者则是通知的订阅者(接受通知)。


posted @ 2008-10-10 11:04 micheal's tech 阅读(383) | 评论 (0)编辑 收藏

为什么C++编译器不能支持对模板的分离式编译
刘未鹏(pongba) /文

首先,C++标准中提到,一个编译单元[translation unit]是指一个.cpp文件以及它所include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp 文件为一个.obj文件,后者拥有PE[Portable Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行,因为并不保证其中一定有main 函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。
举个例子:
//---------------test.h-------------------//
void f();//这里声明一个函数f
//---------------test.cpp--------------//
#i nclude”test.h”
void f()
{
…//do something
} //这里实现出test.h中声明的f函数
//---------------main.cpp--------------//
#i nclude”test.h”
int main()
{
f(); //调用f,f具有外部连接类型
}
在 这个例子中,test. cpp和main.cpp各被编译成为不同的.obj文件[姑且命名为test.obj和main.obj],在main.cpp中,调用了f函数,然而 当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,所以,编译器将这里的f看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj,也就是 说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。在 main.obj中对f的调用只会生成一行call指令,像这样:
call f [C++中这个名字当然是经过mangling[处理]过的]
在 编译时,这个call指令显然是错误的,因为main.obj中并无一行f的实现代码。那怎么办呢?这就是连接器的任务,连接器负责在其它的.obj中 [本例为test.obj]寻找f的实现代码,找到以后将call f这个指令的调用地址换成实际的f的函数进入点地址。需要注意的是:连接器实际上将工程里的.obj“连接”成了一个.exe文件,而它最关键的任务就是 上面说的,寻找一个外部连接符号在另一个.obj中的地址,然后替换原来的“虚假”地址。
这个过程如果说的更深入就是:
call f这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个
jmp 0x23423[这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对 后者的call XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢[在本例中这处于test.obj中],因为.obj于.exe的格式都是一样的,在这 样的文件中有一个符号导入表和符号导出表[import table和export table]其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f[当然C++对f作了mangling]的 地址就行了,然后作一些偏移量处理后[因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚]写入main.obj中的符号导入表中f 所占有的那一项。
这就是大概的过程。其中关键就是:
编译main.cpp时,编译器不知道f的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。
编译test.cpp时,编译器找到了f的实现。于是乎f的实现[二进制代码]出现在test.obj里。
连接时,连接器在test.obj中找到f的实现代码[二进制]的地址[通过符号导出表]。然后将main.obj中悬而未决的call XXX地址改成f实际的地址。
完成。

然而,对于模板,你知道,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“具现化”的过程。举个例子:
//----------main.cpp------//
template<class T>
void f(T t)
{}
int main()
{
…//do something
f(10); //call f<int> 编译器在这里决定给f一个f<int>的具现体
…//do other thing
}
也就是说,如果你在main.cpp文件中没有调用过f,f也就得不到具现,从而main.obj中也就没有关于f的任意一行二进制代码!!如果你这样调用了:
f(10); //f<int>得以具现化出来
f(10.0); //f<double>得以具现化出来
这样main.obj中也就有了f<int>,f<double>两个函数的二进制代码段。以此类推。
然而具现化要求编译器知道模板的定义,不是吗?
看下面的例子:[将模板和它的实现分离]
//-------------test.h----------------//
template<class T>
class A
{
public:
void f(); //这里只是个声明
};
//---------------test.cpp-------------//
#i nclude”test.h”
template<class T>
void A<T>::f() //模板的实现,但注意:不是具现
{
…//do something
}
//---------------main.cpp---------------//
#i nclude”test.h”
int main()
{
A<int> a;
a. f(); //编译器在这里并不知道A<int>::f的定义,因为它不在test.h里面
//于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到
//A<int>::f的实现体,在本例中就是test.obj,然而,后者中真有A<int>::f的
//二进制代码吗?NO!!!因为C++标准明确表示,当一个模板不被用到的时
//侯它就不该被具现出来,test.cpp中用到了A<int>::f了吗?没有!!所以实
//际上test.cpp编译出来的test.obj文件中关于A::f的一行二进制代码也没有
//于是连接器就傻眼了,只好给出一个连接错误
// 但是,如果在test.cpp中写一个函数,其中调用A<int>::f,则编译器会将其//具现出来,因为在这个点上[test.cpp 中],编译器知道模板的定义,所以能//够具现化,于是,test.obj的符号导出表中就有了A<int>::f这个符号的地
//址,于是连接器就能够完成任务。
}

关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找[当遇到未决符号时它会寄希望于连 接器]。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会具现化出来,所以,当编译器只看到模板的声明时,它不能 具现化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的具现体时,编译器 懒得去具现,所以,整个工程的.obj中就找不到一行模板具现体的二进制代码,于是连接器也黔

/////////////////////////////////
http://dev.csdn.net/develop/article/19/19587.shtm
 C++模板代码的组织方式 ——包含模式(Inclusion Model)     选择自 sam1111 的 Blog 
关键字   Template Inclusion Model
出处   C++ Template: The Complete Guide


说明:本文译自《C++ Template: The Complete Guide》一书的第6章中的部分内容。最近看到C++论坛上常有关于模板的包含模式的帖子,联想到自己初学模板时,也为类似的问题困惑过,因此翻译此文,希望对初学者有所帮助。

模板代码有几种不同的组织方式,本文介绍其中最流行的一种方式:包含模式。

链接错误

大多数C/C++程序员向下面这样组织他们的非模板代码:

         ·类和其他类型全部放在头文件中,这些头文件具有.hpp(或者.H, .h, .hh, .hxx)扩展名。

         ·对于全局变量和(非内联)函数,只有声明放在头文件中,而定义放在点C文件中,这些文件具有.cpp(或者.C, .c, .cc, .cxx)扩展名。
 

这种组织方式工作的很好:它使得在编程时可以方便地访问所需的类型定义,并且避免了来自链接器的“变量或函数重复定义”的错误。
 

由于以上组织方式约定的影响,模板编程新手往往会犯一个同样的错误。下面这一小段程序反映了这种错误。就像对待“普通代码”那样,我们在头文件中定义模板:
 

// basics/myfirst.hpp 

#ifndef MYFIRST_HPP
#define MYFIRST_HPP 

// declaration of template

template <typename T>

void print_typeof (T const&);

#endif // MYFIRST_HPP

 

print_typeof()声明了一个简单的辅助函数用来打印一些类型信息。函数的定义放在点C文件中:

// basics/myfirst.cpp

#i nclude <iostream>

#i nclude <typeinfo>

#i nclude "myfirst.hpp"
 

// implementation/definition of template

template <typename T>
void print_typeof (T const& x)
{

    std::cout << typeid(x).name() << std::endl;

}

 

这个例子使用typeid操作符来打印一个字符串,这个字符串描述了传入的参数的类型信息。

最后,我们在另外一个点C文件中使用我们的模板,在这个文件中模板声明被#i nclude:

// basics/myfirstmain.cpp 

#i nclude "myfirst.hpp" 

// use of the template

int main()
{

    double ice = 3.0;
    print_typeof(ice);  // call function template for type double

}

 
大部分C++编译器(Compiler)很可能会接受这个程序,没有任何问题,但是链接器(Linker)大概会报告一个错误,指出缺少函数print_typeof()的定义。

这个错误的原因在于,模板函数print_typeof()的定义还没有被具现化(instantiate)。为了具现化一个模板,编译器必须知道 哪一个定义应该被具现化,以及使用什么样的模板参数来具现化。不幸的是,在前面的例子中,这两组信息存在于分开编译的不同文件中。因此,当我们的编译器看 到对print_typeof()的调用,但是没有看到此函数为double类型具现化的定义时,它只是假设这样的定义在别处提供,并且创建一个那个定义 的引用(链接器使用此引用解析)。另一方面,当编译器处理myfirst.cpp时,该文件并没有任何指示表明它必须为它所包含的特殊参数具现化模板定 义。

头文件中的模板

解决上面这个问题的通用解法是,采用与我们使用宏或者内联函数相同的方法:我们将模板的定义包含进声明模板的头文件中。对于我们的例子,我们可以通 过将#i nclude "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一个使用我们的模板的点C文件中包含myfirst.cpp文件,来达到目 的。当然,还有第三种方法,就是删掉myfirst.cpp文件,并重写myfirst.hpp文件,使它包含所有的模板声明与定义:


// basics/myfirst2.hpp

#ifndef MYFIRST_HPP
#define MYFIRST_HPP 

#i nclude <iostream>
#i nclude <typeinfo>
 

// declaration of template
template <typename T>
void print_typeof (T const&); 

// implementation/definition of template
template <typename T>
void print_typeof (T const& x)
{

    std::cout << typeid(x).name() << std::endl;

}

#endif // MYFIRST_HPP

 

这种组织模板代码的方式就称作包含模式。经过这样的调整,你会发现我们的程序已经能够正确编译、链接、执行了。

从这个方法中我们可以得到一些观察结果。最值得注意的一点是,这个方法在相当程度上增加了包含myfirst.hpp的开销。在这个例子中,这种开 销并不是由模板定义自身的尺寸引起的,而是由这样一个事实引起的,即我们必须包含我们的模板用到的头文件,在这个例子中 是<iostream>和<typeinfo>。你会发现这最终导致了成千上万行的代码,因为诸 如<iostream>这样的头文件也包含了和我们类似的模板定义。

这在实践中确实是一个问题,因为它增加了编译器在编译一个实际程序时所需的时间。我们因此会在以后的章节中验证其他一些可能的方法来解决这个问题。但无论如何,现实世界中的程序花一小时来编译链接已经是快的了(我们曾经遇到过花费数天时间来从源码编译的程序)。

抛开编译时间不谈,我们强烈建议如果可能尽量按照包含模式组织模板代码。

另一个观察结果是,非内联模板函数与内联函数和宏的最重要的不同在于:它并不会在调用端展开。相反,当模板函数被具现化时,会产生此函数的一个新的 拷贝。由于这是一个自动的过程,编译器也许会在不同的文件中产生两个相同的拷贝,从而引起链接器报告一个错误。理论上,我们并不关心这一点:这是编译器设 计者应当关心的事情。实际上,大多数时候一切都运转正常,我们根本就不用处理这种状况。然而,对于那些需要创建自己的库的大型项目,这个问题偶尔会显现出 来。
 

最后,需要指出的是,在我们的例子中,应用于普通模板函数的方法同样适用于模板类的成员函数和静态数据成员,以及模板成员函数。



posted @ 2008-10-09 17:23 micheal's tech 阅读(1839) | 评论 (0)编辑 收藏

#include  <iostream>
using namespace std;
#include <assert.h>
int MergeSort(int A[],int p,int q,int r)
{
    int n1 = q-p+2;
    int n2 = r-q+1;
    int *parray1 = new int[n1];
    int *parray2 = new int[n2];
    assert(parray1);
    assert(parray2);
    for(int i = 0;i<n1-1;i++)
    {
        parray1[i] = A[p+i];
    }
    parray1[n1-1] = 0xffff;
    for(int i = 0;i<n2-1;i++)
    {
        parray2[i] = A[q+i+1];
    }
    parray2[n2-1] = 0xffff;
    for(int i=0,j=0,k=p;k<=r;k++)
    {
        if(parray1[i]>parray2[j])
        {
            A[k] = parray2[j];
            j++;
        }
        else
        {
            A[k] = parray1[i];
            i++;
        }
    }

    delete []parray1;
    delete []parray2;


}
int Merge(int A[],int p,int r)
{
    if(p<r)
    {
       int q = (p+r)/2;
       Merge(A,p,q);
       Merge(A,q+1,r);
       MergeSort(A,p,q,r);
    }
}
int main()
{
    int a[]={5,2,4,7,1,3,2,6};
    Merge(a,0,sizeof(a)/sizeof(int)-1);
    for(int i=0;i<8;i++)
    {
        cout<<a[i]<<" ";
    }
    cout<<endl;
}



posted @ 2008-10-08 15:01 micheal's tech 阅读(844) | 评论 (0)编辑 收藏

句柄类是存储和管理基类指针的一个类
需要句柄类的背景:
1)在对安全要求很高的领域,即使核心实现已经封闭在库中不可见,但头文件中变量定义仍可能曝露一些内部信息
2)在设计初期、实现部分会经常变动,甚至头文件中变量定义也需要经常变动,因此在重编译的时候头文件也需要编译,
有时候导致编译时间过长。
句柄类就是为了解决这类问题
// Handle.h
class Implement; //这里是对真正实现功能的类的声明

class ImplementHandle //这是Implement类的句柄类
{
public:
ImplementHandle():lpImplementInstance(NULL){lpImplementInstance = new Implement;}
~ImplementHandle(){ delete lpImplementInstance ;}
public:
// Interface_FOO() 是句柄类提供的接口,它的真正实现是在Implement类里面,而这个仅仅是接口"代理"
RE_TYPE Interface_FOO()
{
return lpImplementInstance->FOO
_
Implementation(); //句柄代理调用实现真正的FOO接口
}
//还有其他的接口也一样,均是用句柄类代理接口
//....
private:
Implement * lpImplementInstance; //这个是句柄类唯一的数据成员(当然,并不一定),可以被句柄类很好地自动管理
};




  被封装在句柄类里面的真正实现(class Implement)将与用户隔离开来,就是说,就算以后Implement 类的实现有所改变,
只要它提供的接口不变,那么它的句柄类就不会改变,而包含句柄类的用户,也不用做任何相应的改变(所有包含 “Handle.h”的模块甚至不用从新编译就可以正常更新至最新的Implement实现)。

例如
#include "Handle.h"
Imlementhandle testhandle;
testhandle->Interface_Foo();//调用接口即可。
如果改动了Implent类的方法

  于此相反,如果直接用class Implement 的定义,那么只要Implement类的定义有所改变(不管是public 还是private 成员
的更新),那么所有包含它的头文件的模块都要从新编译一次。

这其实就是接口与实现的分离,


posted @ 2008-10-07 16:13 micheal's tech 阅读(1140) | 评论 (0)编辑 收藏

仅列出标题
共8页: 1 2 3 4 5 6 7 8