Creative Commons License
本Blog采用 知识共享署名-非商业性使用-禁止演绎 3.0 Unported许可协议 进行许可。 —— Fox <游戏人生>

游戏人生

游戏人生 != ( 人生 == 游戏 )
posts - 47, comments - 300, trackbacks - 0, articles - 7

置顶随笔

因站点迁移。请订阅本博的朋友将RSS修改为http://www.yulefox.com/?feed=rss2

个人主页地址:http://www.yulefox.com

posted @ 2008-11-10 11:17 Fox 阅读(138) | 评论 (0)编辑 收藏

2008年11月19日

不知道Singleton算不算用的最多的,平时用的时候,往往都是直接敲下面一段:

class CSingleton
{
public:
    static void Initial(void) { if( NULL == m_pInst ) m_pInst = new CSingleton; }
    static void Release(void) { delete m_pInst; m_pInst = NULL; }
    static CSingleton* GetInst(void) { return m_pInst; }

private:
    CSingleton() {}
    ~CSingleton() {}
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton &);
    static CSingleton* m_pInst;
};

不是不想改,就是懒,敲多了已经不觉得这么写多浪费时间了,按大家的说法,这样写至少有这么几个缺点:

1. 必须在程序结束前手动释放,这不仅是RP问题,如果你借了内存不主动还,说明你RP差,但被别人搞丢了(宕机)导致你还不上,说明别人RP差?所以,这还是个问题;

2. 线程同步问题,如果Singleton实例跨线程使用,上例不安全,在Initial和Release时加锁可以解决;

3. 最大的问题:不能重用。

阅读全文

posted @ 2008-11-19 23:37 Fox 阅读(417) | 评论 (0)编辑 收藏

State模式对应到C++的多态特性。

State模式适用较广,这儿给出比较常见易懂的三种情况:

1. 当怪物在面对不同职业和特性的玩家时对应不同的AI处理和技能释放:

CSkill* pAttackSkill;

if( pPlayer->MagicImmune() ) pAttackSkill = SomePhysicalAttackSkill;

else if( pPlayer->PhysicalImmune() ) pAttackSkill = SomeMagicAttackSkill;

...

pAttackSkill->Begin();

...

或者使用分支结构:

CSkill* pAttackSkill;

switch( pPlayer->GetOccupation() )

{

  case WARRIOR: pAttackSkill = SomeLongRangeSkill; break;

  case MAGICIAN: pAttackSkill = SomeForceSkill; break;

  case NIMROD: pAttackSkill = SomeMagicSkill; break;

...

}

pAttackSkill->Begin();

...

阅读全文

posted @ 2008-11-19 00:57 Fox 阅读(642) | 评论 (0)编辑 收藏

2008年11月10日

因站点迁移。请订阅本博的朋友将RSS修改为http://www.yulefox.com/?feed=rss2

个人主页地址:http://www.yulefox.com

posted @ 2008-11-10 11:17 Fox 阅读(138) | 评论 (0)编辑 收藏

之所以要写这个东西,是因为由最近发生的腾讯竞业禁止案发散思维了。借机反思一下员工对企业认同的源与流

之所以要写Google,Google是我的梦,正像我当初在一份简历中写道的:

Game is my interest, so I like developing my game; Google is my life, so I love enjoying my life.

我的工作、学习、生活离不开Google:My indispensable tools: Gmail, G.cn, Code, iGoogle, Picasa, Reader, Talk, Notebook, Map, Calendar, Documents, Google Pinyin, Chrome, etc.

不煽情了,坊间流传的最多的就是富有传奇色彩Google的工作环境和待遇,下图可以说明很多问题了:

阅读全文……

posted @ 2008-11-10 00:13 Fox 阅读(1121) | 评论 (1)编辑 收藏

2008年11月6日

现在每天的工作主要是为了满足项目需求和进度而不停的思考、敲键盘。有时候也确实需要抽点时间来思考思考那些看上去用不到的一些东西,又想起了Fibonacci数

之前曾经三次写过Fibonacci数:2007年4月的我的Fibonacci数列,2007年12月的也说说级数求和(1+2+3…N)和其他,2008年5月的动态规划算法,但给出的都不是非常优的算法。

上次回去把同学借的《编程之美》偷过来还没怎么看,晚上翻了一下,看到有讲Fibonacci数,想起来的The Art of Computer Programming Vol.1也讲过,觉得有必要对Fibonacci数做个了断。

诚如在The Art of Computer Programming Vol.1所述,是中世纪以来欧洲最伟大的数学家,他关于al-Khwarizmi的研究催生了算法(algorithm)一词。

阅读全文

看到这些,我又激动了,数学之美,不正是美在这些地方吗?我们不是要做数学家,但这并不妨碍我们站在门口向里张望……

posted @ 2008-11-06 00:34 Fox 阅读(1044) | 评论 (0)编辑 收藏

2008年10月27日

本文同时发布在

近来在Windows下用WSAEventSelect时,碰到一个棘手的问题,当然现在已经解决了。

问题描述:

一个Server,一个ClientA,一个ClientB,Server用WSAEventSelect模型监听(只有监听,没有读写),ClientA在连接Server后,ClientA对应的EventA被触发,Server的WSAWaitForMultipleEvents等待到EventA,ClientB连接Server时,TCP三次握手成功,ClientB与Server的TCP状态被置为ESTABLISHED,然而Server的WSAWaitForMultipleEvents没有等待到EventB被触发。

用netstat看了一下,ClientB与Server的状态是ESTABLISHED,此时如果ClientB退出,由于Server无法正常Close该连接,因此Server的状态不是TIME_WAIT而是CLOSE_WAIT(持续2小时),Client的状态是FIN_WAIT_2(持续10分钟)。

我尝试将ClientA主动关闭后再次连接Server,Server的WSAWaitForMultipleEvents在wait到EventA之后,EventB此时也被触发。

开始一直以为问题的根源在于WSAEventSelect的使用上,毕竟,之前没有系统写过类似的代码,难免怀疑到事件模型的使用上。多方查阅资料,最后还是没有发现类似问题的解决方案。

又跟了一上午之后,Kevin开始怀疑是多线程使用的问题,我看了一下,的确没有对event的多线程操作进行处理,但因为在另一个应用中,使用了同样的模块,却没有该问题。最后考虑必要性时还是放弃了加临界资源,无视多线程同步问题。Kevin本来劝我换个模型,但我固执的认为要做就把这事儿做好。因为下午还要回学校一趟,就想尽快搞定,毕竟因为这一块已经把Kevin的进度拖了一周了,心下还是过意不去,而且隐约感觉到离问题的解决越来越近了。

问题分析:

在对着WSAWaitForMultipleEvents思考了半天之后,忽然开窍了,如果ThreadA在WSAWaitForMultipleEvents时,只有一个EventA被WSAEventSelect并set到signaled状态,则该EventA会被wait成功,ThreadA处理EventA之后继续阻塞在WSAWaitForMultipleEvents。此时,ThreadB通过WSAEventSelect将EventB初始化为nonsignaled状态,之后即使EventB被set为signaled状态,但ThreadA的WSAWaitForMultipleEvents因为处于阻塞状态,不可能刷新事件集,也就不可能wait到EventB,最终导致了ClientB的请求无法被响应。如果EventA被触发则会被ThreadA等待到,WSAWaitForMultipleEvents返回后再次进入时事件集已经被刷新,EventB被wait到也就不难理解了。

问题解决:

说到底是因为当ThreadA阻塞在WSAWaitForMultipleEvents处之时,事件集的变更无法立即得到体现。如果允许上层应用随时create或close一些event,则WSAWaitForMultipleEvents就不应该无限阻塞下去。

因此最后的一个解决方法就是让WSAWaitForMultipleEvents超时返回并Sleep一段时间,当WSAWaitForMultipleEvents再次进入时事件集得以更新。

想了一下,另一个应用中之所以没出现该问题也只是个巧合,因为该应用中ThreadB的两次WSAEventSelect间隔很短,在ThreadA获得时间片之前已经确定了事件集。

说白了这也不是一个什么大问题,甚至谈不上任何难度,但是因为之前对WSAEventSelect没有一个清晰的概念,因此在发现和分析问题上花费了大量时间,加上在VS2005调试过程中,有个别文件更新时没有被重新编译,也耗费了很多无谓的时间,以至于我们都在考虑是不是要放弃IDE,因为我们确实太依赖IDE了,有些TX为了稳妥,每次都是“重新生成整个解决方案”,如果一个解决方案有几千个文件、几十万行的代码,估计重编一次也要花个几分钟吧。

总结:

  1. netstat观察的网络连接处于ESTABLISHED状态并不意味着逻辑连接被accept,只是表明客户端connect的TCP物理连接(三次握手)被服务器端ack,如果服务器没有accept到该连接,证明网络模块代码有问题;
  2. 多线程怎么都是个问题,线程同步尽量避免,毕竟,用Kevin的话来说,加锁是丑陋的。但在涉及到同步问题时,还是权衡一下,我这儿之所以最后没有加临界区,是因为事件主要是在ThreadA中处理,ThreadB中只有create操作,而且ThreadA对事件集的刷新要求不是那么严格,也就不考虑加临界区了;
  3. 如果能力和条件允许的话,放弃IDE吧,IDE的确不是个好东西,我主要是指在编译链接的时候,如果作为编辑器说不定还会好用:)。

个人网站用的主机最近从据说要黑屏的Windows换成了Debian,还在调整,估计明天能弄好,内容肯定比Cppblog杂的多,谈点技术的还是会同步更新到

posted @ 2008-10-27 23:25 Fox 阅读(1378) | 评论 (1)编辑 收藏

2008年10月10日

作者:Fox

本文同时发布在http://www.yulefox.comhttp://www.cppblog.com/fox

十天之前,在CPPBLOG上写了一篇,有同学提到该实现不支持成员函数。这个问题我也考虑到了,既然被提出来,不妨把实现提供出来。

需要说明的是,我本身对template比较不感冒,不过对template感冒,而且写过关于成员函数指针的问题,想了很久,如果支持成员函数指针,不用模板是不行了。

此处对成员函数的支持还不涉及对函数参数的泛化,因为我这个消息映射暂时不需要参数泛化,下面的代码应该不需要过多的解释了。

#define REG_MSG_FUNC(nMsgType, MsgFunc) \
    CMsgRegister::RegisterCallFunc(nMsgType, MsgFunc);

#define REG_MSG_MEM_FUNC(nMsgType, Obj, MsgFunc) \
    CMsgRegister::RegisterCallFunc(nMsgType, Obj, MsgFunc);

class CBaseMessage;

class CHandler
{
public:
    virtual int operator()(CBaseMessage* pMsg) = 0;
};

template<typename FuncType>
class CDefHandler : public CHandler
{
public:
    CDefHandler(){}
    CDefHandler(FuncType &Func)
        : m_Func(Func)
    {
    }

    virtual int operator()(CBaseMessage* pMsg)
    {
        return m_Func(pMsg);
    }

protected:
    FuncType    m_Func;
};

template<typename ObjType, typename FuncType>
class CMemHandler : public CHandler
{
public:
    CMemHandler(){}
    CMemHandler(ObjType* pObj, FuncType Func)
        : m_pObj(pObj)
        , m_Func(Func)
    {
    }

    virtual int operator()(CBaseMessage* pMsg)
    {
        return (m_pObj->*m_Func)(pMsg);
    }

protected:
    FuncType    m_Func;
    ObjType*    m_pObj;
};

class CFunction
{
public:
    CFunction()
        : m_pHandler(NULL)
    {
    }

    // 封装(C函数或静态成员函数)
    template<typename FuncType>
    CFunction( FuncType &Func )
        : m_pHandler(new CDefHandler<FuncType>(Func))
    {
    }

    // 封装(非静态成员函数)
    template<typename ObjType, typename FuncType>
    CFunction( ObjType* pObj, FuncType Func )
        : m_pHandler(new CMemHandler<ObjType, FuncType>(pObj, Func))
    {
    }

    virtual ~CFunction()
    {
        DELETE_SAFE(m_pHandler);
    }

        // 函数调用
    int operator()(CBaseMessage* pMsg)
    {
        return (*m_pHandler)(pMsg);
    }

private:
    CHandler    *m_pHandler;
};

typedef std::map<int, CFunction*> MSG_MAP;
typedef MSG_MAP::iterator MSG_ITR;

class CMsgRegister
{
public:
    // 注册消息函数(C函数或静态成员函数)
    template <typename FuncType>
    inline static void RegisterCallFunc(int nMsgType, FuncType &Func)
    {
        CFunction *func = new CFunction(Func);
        s_MsgMap[nMsgType] = func;
    }

    // 注册消息函数(非静态成员函数)
    template <typename ObjType, typename FuncType>
    inline static void RegisterCallFunc(int nMsgType, ObjType* pObj, FuncType Func)
    {
        CFunction *func = new CFunction(pObj, Func);
        s_MsgMap[nMsgType] = func;
    }

    // 执行消息
    inline static void RunCallFunc(int nMsgType, CBaseMessage* pMsg)
    {
        MSG_ITR itr = s_MsgMap.find(nMsgType);
        if( s_MsgMap.end() != itr )
        {
            (*itr->second)(pMsg);
        }
    }

    static void ReleaseMsgMap()                // 释放消息映射表
    {
        MSG_ITR itr = s_MsgMap.begin();
        while( itr != s_MsgMap.end() )
        {
            DELETE_SAFE(itr->second);
            itr = s_MsgMap.erase(itr);
        }
    }

protected:
    static MSG_MAP            s_MsgMap;        // 消息映射表
};

不可否认,模板给了你更大的想象空间,很多东西,还是不要一味排斥的好:)。

posted @ 2008-10-10 10:20 Fox 阅读(1224) | 评论 (4)编辑 收藏

作者:Fox

本文同时发布在http://www.yulefox.comhttp://www.cppblog.com/fox

两个多月之前,在CPPBLOG上写过一篇关于游戏开发中的问题,主要该考虑的问题都已经说明,当时没有实现这一块。在最近一个模块中,写了一个非常简单的写日志的接口,接口的声明大概是:

void PutoutLog(const char *szFile, const char *szLog, ...);

记录的日志格式如下:

1  2008-10-10-03:30:10.618 | projectpath/srcfile.cpp/function(30) : 哦嚯, 这儿出错了(eno : 0x00100000).

用到了__FILE__、__LINE__、__FUNCTION__几个宏。

基本满足需要了,需要改进的地方我现在能想到的主要是:

  • 文件名是全路径,没有必要,只记录文件名称其实就够了;
  • 没有考虑写日志时的线程同步问题;
  • 系统dump时的日志还是没有办法记录;
  • 缺少足够的、动态的上下文信息:调用堆栈、函数参数、系统运行参数;
  • 日志记录到普通文本中,虽然记录了时间、位置,还是不便于系统查看、查找、分析、挖掘。

说白了,这所谓的基本满足需要只是皮毛,因为最近在打理,有感于网页数据库技术的博大精深、美妙直观,如果可以把日志用网页数据库作为读写的载体,岂不甚妙?

隐约中感觉这种想法似曾相识,不识字只好乱翻书,果然在中发现有这样一篇文章:一个基于HTML的日志和调试系统。有兴趣的同学自己翻书吧:)。

如果将更加丰富的信息写入xml或php文件中,加入到数据库,可以对数据进行分析、挖掘,并友好的显示在浏览器中,这对于枯燥的debug过程,起码增添了些许益处。

然而,这又只是一个想法,或许在我手头上的工作稍后告一段落的时候,我可以花精力研究一下这方面的东西。

posted @ 2008-10-10 04:18 Fox 阅读(1264) | 评论 (8)编辑 收藏

2008年9月29日

项目中使用了消息通信机制,因为消息类型非常多,相应的,处理消息的地方代码也非常多。

自然而然想到MFC中的消息映射:

创建一个缺省MFC框架程序的解决方案Test,在Test.h中看到以下内容:

class Ctest_mfcApp : public CWinApp
{
public:
    Ctest_mfcApp();

// 重写
public:
    virtual BOOL InitInstance();

// 实现
    afx_msg void OnAppAbout();
    DECLARE_MESSAGE_MAP()
};

 

其中,最紧要的就是DECLARE_MESSAGE_MAP()这个宏,相关内容展开如下:

struct AFX_MSGMAP_ENTRY
{
    UINT nMessage;   // windows message
    UINT nCode;      // control code or WM_NOTIFY code
    UINT nID;        // control ID (or 0 for windows messages)
    UINT nLastID;    // used for entries specifying a range of control id's
    UINT_PTR nSig;   // signature type (action) or pointer to message #
    AFX_PMSG pfn;    // routine to call (or special value)
};

struct AFX_MSGMAP
{
    const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
    const AFX_MSGMAP_ENTRY* lpEntries;
};

#define DECLARE_MESSAGE_MAP() \
protected: \
    static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \
    virtual const AFX_MSGMAP* GetMessageMap() const; \

其中AFX_PMSG不再解析下去,我们认为这是一个指向特定消息对应的实现函数的函数指针,这几个宏的作用可简单理解成为Test这个项目定义了一个静态的消息映射表。当消息到来时,从消息队列中弹出消息并分发到具有入口实现的上层CWnd派生窗口。用户只需要注册消息,实现消息入口函数就够了,这在MFC中一般放在.cpp文件头部。Test.cpp中头部有以下内容:

BEGIN_MESSAGE_MAP(CTest, CWinApp)
    ON_COMMAND(ID_APP_ABOUT, &CTest::OnAppAbout)
    // 基于文件的标准文档命令
    ON_COMMAND(ID_FILE_NEW, &CWinApp::OnFileNew)
    ON_COMMAND(ID_FILE_OPEN, &CWinApp::OnFileOpen)
    // 标准打印设置命令
    ON_COMMAND(ID_FILE_PRINT_SETUP, &CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()

这里是为消息枚举值与消息实现函数建立映射,其中涉及到的宏的展开如下:

#define ON_COMMAND(id, memberFxn) \
    { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, \
        static_cast<AFX_PMSG> (memberFxn) },

#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
    PTM_WARNING_DISABLE \
    const AFX_MSGMAP* theClass::GetMessageMap() const \
        { return GetThisMessageMap(); } \
    const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
    { \
        typedef theClass ThisClass;                           \
        typedef baseClass TheBaseClass;                       \
        static const AFX_MSGMAP_ENTRY _messageEntries[] =  \
        {

#define END_MESSAGE_MAP() \
        {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
    }; \
        static const AFX_MSGMAP messageMap = \
        { &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \
        return &messageMap; \
    }                                  \
    PTM_WARNING_RESTORE

按照上述思路得到的相似代码如下:

// Test.h
typedef void (* funCall)(void*);        // 消息执行函数类型

struct tagMsgEntry                      // 消息入口结构
{
    int            nMsgType;            // 消息类型
    funCall        MsgRun;              // 消息执行函数
};

struct tagMsgMap                        // 消息映射表结构
{
    const tagMsgMap* (__stdcall* funGetBaseMsgMap)();
    const tagMsgEntry* pMsgEntries;     // 消息入口集
};

class CMessage
{
    // ...
protected:
    static const tagMsgMap* __stdcall GetThisMsgMap();
    virtual const tagMsgMap* GetMsgMap() const;
};

// Test.cpp
const tagMsgMap* CMessage::GetMsgMap() const
{
    return GetThisMsgMap();
}

const tagMsgMap* __stdcall CMessage::GetThisMsgMap()
{
    static const tagMsgEntry MsgEntries[] =
    {
        { MSG_SOME_ONE, SomeOneFunc },
        { MSG_SOME_TWO, SomeTwoFunc },
        { MSG_NULL, NULL }
    };
    static const tagMsgMap msgMap =
    {
        &CBaseMessage::GetThisMsgMap,    // 父类消息映射表
        &MsgEntries[0]
    };
    return &msgMap;
}

int CMessage::MsgProc(int nMsgType)
{
    switch( nMsgType )
    {
    case MSG_SOME_ONE:
        {

        }
        break;
    }
    return CBaseMessage::MsgProc(nMsgType);
}

这种处理的优点在于,子类没有定义的消息实现接口,可以使用父类接口实现。不过在现在的消息处理中,我们一般不需要由基类来完成,因此可以不依赖基类接口,使用宏可以使代码看上去更加简洁。

___________________________________________________________

简化版本的消息映射采用以下方式,简单清晰:

// Test.h
#define REG_MSG_FUNC(nMsgType, MsgFunc) \
    CMessge::RegisterCallFunc(nMsgType, MsgFunc); \

typedef void (* function)(void*);

typedef std::map<int, function> MSG_MAP;
typedef MSG_MAP::const_iterator MSG_CITR;

class CMessage
{
    // ...
public:
    static const MSG_MAP& GetMsgMap();
    static void RegisterCallFunc(int nMsgType, void(* Func)(void *))
    {
        s_MsgMap[nMsgType] = Func;
    }

    int CMessage::Run(int nMsgType)                // 消息公用执行函数
    {
        MSG_ITR itr = s_MsgMap.find(nMsgType);
        if( s_MsgMap.end() != itr )
        {
            itr->second(this);
        }
    }

protected:
    static MSG_MAP            s_MsgMap;            // 消息映射表
};

// UserTest.cpp -- 用户在使用时对自己关心的消息予以注册, 入口函数予以实现即可
REG_MSG_FUNC(MSG_SOME_ONE, SomeOneFunc)

void SomeOneFunc(CBaseMessage *pMsg)
{
    return;
}

___________________________________________________________

最近忙的焦头烂额,正好写到消息,稍微整理一下,提供些许借鉴。

posted @ 2008-09-29 18:34 Fox 阅读(1661) | 评论 (3)编辑 收藏

2008年9月11日

网络编程学习和实践的过程中,同步(synchronous)/异步(asynchronous)阻塞(blocking)/非阻塞(non-blocking)总是会迷惑很多人。依然记得我半年之前在记述IOCP时,一句不经意的“非阻塞I/O则是致力于提供高效的异步I/O”便引来一番口水论争。

今天在查一些资料的时候,看到关于这几个词的论辩竟不是一般的多,细细想来,这个问题似乎也确实有解释的必要,不在于争论对错,而在于辨明是非。

讨论之前,先限定讨论的范围:此处之同步/异步仅限于I/O操作,与OS所讨论的进程/线程中的其他同步/异步没有直接关系;讨论的内容是:两对相似的术语之间的区别到底有多大

  • 非常大:

Douglas C. Schmidt在《C++网络编程》中这样说到:

They are very different, as follows:

AIO is "asynchronous I/O", i.e., the operation is invoked asynchronously and control returns to the client while the OS kernel processes the I/O request.  When the operation completes there is some mechanism for the client to retrieve the results.

Non-blocking I/O tries an operation (such as a read() or write()) and if it the operation would block (e.g., due to flow control on a TCP connection or due to lack of data in a socket), the call returns -1 and sets errno to EWOULDBLOCK.

翻译如下:

:例如,操作被异步调用时,控制权交给客户端,I/O操作请求则交由操作系统内核处理,当操作完成后,通过某种机制将结果通知客户端。

非阻塞I/O:尝试调用某操作,如果操作被阻塞,则调用返回-1并置错误值为EWOULDBLOCK。

从这两段“very different”的解释来看,我的感觉是并没有指出二者的区别,因为我们无法确定所谓AIO是如何处理的,如果AIO直接“调用返回-1并置错误值为EWOULDBLOCK”,实现“控制权交给客户端”,似乎并无任何不妥。况且,对于非阻塞I/O,我们也需要“当操作完成后,通过某种机制将结果通知客户端”这样的处理。

  • 无差别:

而在Wikipedia上则直接等同二者:Asynchronous I/O, or non-blocking I/O, is a form of input/output processing that permits other processing to continue before the transmission has finished.

当然,对于recv和send,我们一般会说他们是阻塞起的,而不会说他们是同步起的,但这显然不是二者的区别,因为我们都知道,阻塞的原因正是等待同步结果的返回

因此,二者的区别在于,阻塞/非阻塞是表现,同步/异步是原因,我们说某某操作是阻塞起的,或者某某线程是阻塞起的,是因为在等待操作结果的同步返回;我们说某某操作是非阻塞的,是因为操作结果会通过异步方式返回。

讨论到这儿,再咬文嚼字的争辩下去似乎已经没有任何实际意义。

------------------------------------------------------------

PS:纠结一些必要的概念是为了加深理解,太过纠结了反倒会滞塞理解。我之前对于其概念也并非特别清楚,所以才会再续一篇特意言明,也算弥补一下自己的过失。

posted @ 2008-09-11 01:11 Fox 阅读(1679) | 评论 (8)编辑 收藏