国内一些网游的乱糟代码,或是面向过程的设计,或是夸张的本分守己,或是故弄玄虚,查看这些代码,不仅耗时又无所得,糟糕者甚至有“和破棋篓子下棋,越下越臭”的味道,是以读代码应读好代码,才能事半功倍。SUN代码是我所见过最值得研究的代码之一。之后将陆续记录查看该系列代码所得。附带说一下,我这里拿到的是2005年10月29日的1.02版本代码,服务器+客户端+引擎(带部分美术资源),总大小为1.24G。其他版本必有所出入。

一:DLL导出习惯以及内存泄漏Dump

SUN引擎部分非常习惯使用动态链接库,将各个模块分割开发以便扩展更换。

和国内动态库设计不同,SUN的动态导出很少大量使用 EXPORT_DLL 对类和函数进行导出,除了物理系统部分,大部分DLL库仅有一个函数进行导出,多半如下:

extern "C" __declspec( dllexport ) void* Func( ENUM p_eStyle, void* p_pParam ){

   //BreakPointOnMemoryLeak( 41);

   StartMemoryLeakCheck();

   switch( p_eStyle )

   { case eXXX_1:    m_XXX = new CXXX(); m_XXX->DoSth( p_ppParam ); }
}

利用一个函数,传入两个参数,根据第一个枚举参数进行不同的操作,而第二个参数是根据第一个参数进行强制转换为不同类型的指针已协作进行操作,这是个很狡猾的设定,一个简陋的工厂就这么出现了

这点相信有点设计模式的人就能够简单看懂。

不过,这里的DoSth看似简单,却被SUN程序员用的出神入化,这么一个简单的接口,不仅仅是管理器的生成,连大量的操作都被封装进去了,例如:

// 1:创建管理器功能( 最常见的DLL接口功能 )

CWzBase* pBaseManager = NULL; // CWzBase是CWzAnimationManager的基类

case CW_NEW_WZD_ANIMATION:

pBaseManager = new pBase( static_cast<char*> p_pParam );

return pBaseManager ; // 注意接口函数返回值不是void,而是void*

// 2: 销毁管理器功能

case CW_DELETE_WZD_ANIMATION:

pBaseManager = (CWzBase* )p_pParam;

delete [] pBaseManager;

// 3: 控制功能

case CW_SET_COLOR:

pBaseManager->SetColor( (WzColor*)p_pParam );

// 4: 获取功能

case CW_GET_COLOR:

pBaseManager->GetColor( (WzColor*)p_pParam );

看看如何强悍……一个接口加一个枚举,顶多少接口下去……>_<懒人的福音?

我们还可以值得注意的是

//BreakPointOnMemoryLeak( 41);

StartMemoryLeakCheck();

这两句。他们是负责内存检查的。我们看一下源代码先:

#ifdef _DEBUG
#include <crtdbg.h>
#define _CRTDBG_MAP_ALLOC
inline void StartMemoryLeakCheck( void) { _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); }
#define BreakPointOnMemoryLeak( iMemory) _CrtSetBreakAlloc( iMemory)
#else
#define StartMemoryLeakCheck() {}
#define BreakPointOnMemoryLeak {}
#endif

这是使用VC++的CRT调试堆函数进行内存泄漏检查的方法,恩,简单说明一下

#include 是必须的,调用编译器函数 _CrtSetDbgFlag 和 _CrtSetBreakAlloc 的必需。

#define _CRTDBG_MAP_ALLOC 是CRT库定义的一个宏,我们只需定义该宏,则会在抛出泄漏信息时连同泄漏处所在文件以及行号显示出来。

// 未定义_CRTDBG_MAP_ALLOC宏时,使用_CrtDumpMemoryLeaks()在Debug输出控制台将输出下列信息:

Detected memory leaks! Dumping objects -> {18} normal block at 0x00780E80, 64 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete.

// 定义该宏后,使用_CrtDumpMemoryLeaks()在Debug输出控制台将输出下列信息:

Detected memory leaks! Dumping objects -> D:\TestLeak.cpp(30) : {18} normal block at 0x00780E80, 64 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete.

上下比较,仅仅是多了一个泄漏点的文件名以及行号,建议通常定义该宏。

上面,我们是使用使用_CrtDumpMemoryLeaks()函数进行内存泄漏的输出控制台信息输出。但是这个函数是需要在程序出口时使用的。若我们的程序有多个程序出口,例如:游戏中选择“退出”键,按主窗口右上的X关闭,MessageBox关闭,Alt+F4关闭等多种可能出现程序出口的时候,我们不便在每个出口处调用一次该函数,于是,VC++提供了一个简便的方法,就是SUN中常用的那一句:

inline void StartMemoryLeakCheck( void) { _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); }

_CrtSetDbgFlag()函数会自动在每个函数出口处设置一个_CrtDumpMemoryLeaks()以检查内存泄漏。

上面的错误信息会在DEBUG模式下,程序运行完毕后,在VC的“输出”窗口显示该Dump的内存块信息。

我们可以看到之前例子中的Dump信息中,有个{18},这个是内存分配编号,通常来说,一段没有变动的代码,在编译器不变的情况下,内存的分配顺序是确定的,即,它的内存分配编号也是相对固定的,即使是多线程,也会经常性的按顺序分配。

有时CRT并不如我们想象中那么强大,我们无法获得内存泄漏处的源文件名和行号的时候还是常有的,当我们遇到无法映射到源文件的内存泄漏时,我们只好使用_CrtSetBreakAlloc()函数来断点,它会监视指定内存分配编号,当程序在分配指定编号的内存块时,该函数会断点断掉,此时我们通过“调用堆栈”可以迅速找到内存泄漏的位置。这就是上面SUN代码中大量

//BreakPointOnMemoryLeak( 41);

的原因了,看来SUN的程序也很是对41编号内存块泄漏头疼了一阵吧:)笑。

二:单键陷阱。

我们首先来看SUN中的单键实现

template <class T>
class CSingleton
{
public:
static T* s_pSingleton;

CSingleton()
{
   assert( !s_pSingleton);
   int iOffset = ( int)( T*)1 - ( int)( CSingleton <T>*)( T*)1;
   s_pSingleton = ( T*)( ( int)this + iOffset);
}
virtual ~CSingleton()
{
   assert( s_pSingleton);
   s_pSingleton = NULL;
}

static T* GetSingleton( void)
{
   return ( s_pSingleton);
}
};


template <class T>
T* CSingleton<T>::s_pSingleton = NULL;

其他的不必说明,我们仅将眼光放在这里int iOffset = ( int)( T)1 - ( int)( CSingleton )( T*)1;

这是一句令人迷惑的语法,实际作用是有两点:

1:这个表达式是为了适应不同的编译环境和规则才存在的。我们知道,通常来说,指针的大小是4个字节(标准int长度相同),但这个标准根据平台不同,int长度并非确定的。例如,16位系统中指针为2字节,32位为4字节。

2:这个表达式考虑了类多重继承的可能,所以指针寻址的时候是进行的下递增检查。

……嘛,再不清楚的话看看VC++内存分配规则:)

……SUN里陷阱是一堆一堆的啊。