C++基本功和 Design Pattern系列(10-11)

======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书
籍如果文中有错误和遗漏, 请指出,Aear会尽力更正,
谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================

在OO中Base Class最多的应用有2种方式,第一种是interface,也就是基类是个abstract class, 最典型的例子是Factory Design Pattern. 第二种方式是Base Class 提供一种机制,然后提供几个Virtual Function,允许使用者对这个类进行重载,从而达到“配置” (customization)的目的。 其典型的代表是 MFC,通过继承提供消息传递机制的CWin类的OnPaint等函数,实现更多更强大的功能。

这次就主要讲讲这两种类的设计。

============== Abstract Interface ==============

对于Interface Class来说,基类一定是个Abstract Class. 其含义是一类物体的抽象概念。比如下面一个例子:

class Gun {
public:
    void Shoot    (void) = 0;
    ...
};

Gun是所有人的抽象概念,具体的实现起来可以为Eagle, AK47, MD5 等等。所有的枪都有射击的功能,但是他们具体射击的方式不同, 因此只要提供一个Shoot的Interface,内容由派生类来实现。

在具体设计 Interface Class的时候,有几个需要注意的内容:

1. 最好只提供 pure virtual function。 这样做能够使Interface Class更加单一,更加纯净,更加漂亮,更容易维护。Interface就只能是Interface,不应该提供其他任何东西。因为Interface仅仅是一个描述。

2. 最好没有data member。Data Member往往会限制 Interface Class的灵活性。而且有Data Member,一般就要有Accessor Function,使得Interface Class不再纯洁。。。。。。

3. 如果不得不用Data Member,那么一定要提供带参数的Constructor,并且把确省的Constructor设成private.例如:

class
 InterfaceClass {
private:
    UINT32   _dataMember;
public:
    Base()      {};
    ...// other interface      
};

class Derived : public
 InterfaceClass {
public:
    Derived()    {};  
};

上面代码中的Derived Constructor是合法的,但是由于写代码的人粗心大意,忘记给_dataMember附初值,很容易造成程序的崩溃。所以,对于有data member的Base Class 正确的写法是:

class
 InterfaceClass {
private:
    UINT32   _dataMember;
public:
    Base( UINT32 data ) : _dataMember(data) {};
    ...//other interface
};

4. 在大部分情况下,Base Class的Destructor 一般要放到public 里边,并且要有实现,也就是说至少是空的实现,"{ }",即使是pure virtual destructor,这个 {}也是不能少的。因为还很多情况下,都需要通过Interface 调用Destructor来释放object. 如:

class InterfaceClass {
public:
    ~InterfaceClass() {};  
};

======= Derived Class的管理 =======
对于Interface Class来说,如果Derived Class太多,是很难管理的,不过我们可以通过统一的ClassManager来实现。下面是个小例子:

// Interface
class InterfaceClass {
public:
    virtual void SomeInterface( void ) = 0;

private:
friend class ClassManager;  
    ~InterfaceClass()     = 0     {};  
};

// First Concrete Derived Class
class Derived1 : public InterfaceClass {
public:
    virtual void SomeInterface( void )    { ... };

private:
friend class ClassManager;  
   
 Derived1()   {...};
    ~Derived1()   {...};
};

// Second Concrete Derived Class
class Derived2 : public InterfaceClass {
public:
    virtual void SomeInterface( void )    { ... };

private:
friend class ClassManager;  
   
 Derived2()   {...};
    ~Derived2()   {...};
};

// Class Manager
// Singleton
class ClassManager
{
public:
    static ClassManager* GetInstance( void )
    {
       static ClassManager instance;
       return &instance;
    }
   
   
 InterfaceClass * GetDerived1(void) { return new Derived1() };
    InterfaceClass * GetDerived2(void) { return new Derived2() };  

private:
    ClassManager();  
    ~ClassManager();
};

============== Concrete Base Class ==============

Concrete Base Class是使用的比较多的。游戏中经常有的object之间,差别非常的小,这些都可以通过Concrete Base Class来抽象,提高代码重用的效率。比如 Design Pattern中比较著名的 Strategy Pattern. 下面是 Strategy Pattern的小例子:

class StringArray {
public:
    void AddString() {...};
    void SearchString() {....};
    void SortString( void ) { StrategySortString(); };
    ...
private:
    virtual StrategySortString ( void )
    { ... };   // bubble sort
};  

class StringArray2 : public StringArray {
    ...
private:
    virtual StrategySortString ( void )
    { ... };   // quick sort
};  

假设我们有StringArray, 已经提供了所有基本的String Array操作,不过对于不同的Array输入方式,删除方式等,对应的排序方式的效率也不大相同。因此我们可以把sort设置成为 virtual,然后对于不同的StringArray的使用方式,再相对选择正确的Derived Class就可以了。

对于Concrete Base Class 有几点需要注意的:

1. Destructor。 相信这个不用多说了,一定要 virtual, 而且要保证资源释放的干净。

2. 使用non-virtual interface. 这个是C++大师们的结论。 其原因是因为Derived class在重载Virtual的时候,在很多情况下会影响到基类里提供的机制的正常运作。如果使用non-public virtual,可以使得程序更加灵活,并且进程pre/post condition的检查。比如:

class StringArray {
public:
    void SortArray ( )
    {
       // Pre-condition Check
       StrategySoryArray();
       // Post-condition Check
    };  
private:
    virtual void StrategySoryArray( void ) { ... };
};

这样通过 pre/post condition check, 可以最大程度的保证基类的使用者不会犯错误。从而避免程序bug.

3. 尽量使用 private virtual. Protected virtual 只有在继承类需要调用基类实现的时候,才应该放在protected里。比较著名的是clone函数:

    class Base {
    protected:
       Base * clone ( void ) {};
    };

    class Derived : Base{
    protected:
       Base * clone ( void ) { Base::Clone(); ... };
    };


还有其他的一些方面,那就和基本的class设计大同小异了。 不过还有一个重要的方面,就是Operator 和 Copy Constructor.

============== Operator ==============

在Base Class里边,所有的 operator中最重要的就是operator =了。对于 Abstract Class 来说,最好是禁止使用 Operator =。 为什么呢? 让我们来看看代码:

class Base {
public:
    virtual void Interface( void ) = 0;
    ...  
};

class Derived1
 : public Base{
private:
    _x;
};

class Derived2 : public Base{
private:
    _x;
    _y;
};


Base *pD1 = new Derived1();
Base *pD2 = new Derived2();

*pD1 = *pD2;

也许 *pD1 = *pD2并不常见,但是确实是合法的代码,可以通过编译器的检查。但是*pD1 = *pD2 调用的是 Base 的 operator = , 在Base 并没有提供operator =的情况下,使用的是缺省的位拷贝操作。这个缺省的拷贝操作只拷贝Base Class的内存,而忽略了Derived Class的内存。也就是说 _x _y都没有被拷贝。

如果我们在 Base Class 里提供一个 virtual operator =, 在一定程度上能够解决问题,但是 overload 的operator 必须进行类型转换,例如:

Base & Derived1::Operator = (Base & x) {
    ....
    static_cast<
Derived1 *>(x)
    ....
};

但是下面的代码仍然可以编译通过,产生错误的结果:

*pD1 = *pD2;

因此,对与 abstract class 来说,最方便的就是禁止 operator = , 把它设成 private.

对于Concrete Base Class来说,要么提供 operator = ,要么放在private 里,但是要在Derived Class里调用 Base Class 的 operator = 在例如:

class Base {
    ...
public:
    Base & operator = ( Base & );
};

class Derived : public Base{
    ...
public:
    Derived & operator = ( Derived & X )
    {
       Base::operator = ( X );
       ...
       // own assignment operations
    }
};

这种方法不支持cast up,也就是:
    Base * pBase1 = new Derived();
    Base * pBase2 = new Derived();
    *pBase1 = *pBase2;
   
稍微好点的方式是把 Base 的 operator = 放在 protected里边,但是也不是解决根本的问题。最好的解决方案是使用前面讲到的virtual clone 和 construct实现。如:

class Base {
    virtual Base * construct (void) { return new Base(); };
    virtual Base * clone(void) { return new Base(*this); };
};

class Derived : public Base {
    virtual Base * construct (void)
    { return new Derived(); };
   
    virtual Base * clone(void)
    { return new Derived(*this); };
};

同时禁止Base的 opeartor =。 但是这样的效率会有所降低。总之,最好的方法是使用 abstract interface,同时禁止base使用 private operator =.


好了。这次就说这么多,希望对大家有帮助,下次见!还有,虽然不是我们的节日,但是大家圣诞快乐!

 

 

 

======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书
籍如果文中有错误和遗漏, 请指出,Aear会尽力更正,
谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================

说到Exception就要说下相关的Error Handling. 比较常用的Error Handling一般有如下几种类方式:
    1. Return Value
    2. Assert
    3. Debug Output
    4. Exception

相对于其他三种错误处理方式, Exception更加容易使用,而且使得错误代码相对集中,同时使得独立函数库的开发更加方便。 同样,对于C++来说, Exception提供了Class的Constructor 和 Operator = 错误处理机制,因为这两者都不是能够通过return value进行报错的。

但是就游戏开发来说, Exception最大的缺点是内存和CPU的开销。当然,不是说游戏的代码中不应该使用Exception。 Aear见过用Exception的游戏代码,也有完全不用Exception的代码。因为对游戏来说,应该在运行过程中保持自身状态的正确性,不应该产生任何的无法处理的Exception。 而所有能够自己处理的错误情况,都是能够通过Return Value 来解决的。 唯一可能产生Exception的地方,就是系统资源,比如磁盘文件,网络等。不过大部分系统掉用都提供非Exception的错误处理。不过程序开发各不相同,用不用Exception可能还是需要大家自行决定。

Aear个人观点是能不用Exception,就不用Exception,
但是应该用Exception的时候,一定不要省。比如constructor里。

============ Exception的用法 ============

要使用Exception, 要么用系统的Exception的类,要么定义自己的类。 在定义自己类的时候,可以继承STD里边的Exception类,也可以创建自己新的类。比如:

class ExceptionBase{
    ...
};

class ExceptionDerived : public ExceptionBase{
    ...
};

需要注意的是,通常定义自己的Exception类的时候,都要有一个公共的Base Exception Class, 这样能够保证写代码的时候catch所有的你自定义的Exception,比如:

try {
    ...
}catch( ExceptionDerived & e ) {
    ...
}catch( ExceptionBase & e ) {

    // Catch 其他的Exception, 这样的设计即使今后添加新的Exception,只要
    // 是从ExceptionBase继承来的,都会被catch到。

}catch( ... ) {

    // 这里最好再加上 catch(...)来catch所有的exception,防止有未catch的    // exception. 因为如果有unexpected exception, C++的缺省动作是直接
    // 终止程序的运行。

};

============ Exception in Constructor ============

如果一个Constructor产生exception而且没有被程序catch到的话,那么这个object的创建就会失败, 比如:

class MemoryBlock {
private:
    void * _pMem;
public:
    MemoryBlock ( UINT32 size )
    {
       _pMem = new char[size];
    };
    ....
};

MemoryBlock myMemory(100000000000000000000000000);

如果new在分配内存的过程中throw一个Exception ,通常是 bad_alloc, 那么myMemory的创建就会失败,以后任何对 myMemory的成员访问,都是非法的,会导致程序的崩溃。

让我们看看另一中写法:

class MemoryBlock {
private:
    void * _pMem;
public:
    MemoryBlock ( UINT32 size ) :
       _pMem(new char[size])
    { };
    ....
};
上面也是合法的,不过会产生同样的问题。但是区别在于如果在代码中catch到exception,那么第一种写法,能够保证object被创建,而第二种写法不能。比如:

    // MemoryBlock 能够被创建
    MemoryBlock ( UINT32 size )
    {
       try {
           _pMem = new char[size];
       } catch(...) {}
    };


    // MemoryBlock 创建失败
    MemoryBlock ( UINT32 size )
    try
      
 : _pMem(new char[size])
    { } catch(...) {};



============ Exception in Destructor ============

其实对于Destructor来说就一句话,不能在Destructor中Throw Exception。 原因很简单,因为通常Destructor要么在Delete Object中掉用,要么在已经Throw了Exception的时候,由系统掉用。如果在Throw Exception的情况下再Throw Exception的话,那么程序就会强制终止。

============ Exception in Operator ============

这个是比较麻烦的,通常的Exception的处理有好几个级别, Basic, Strong, Nofail.我们这里只说下Strong Exception Safety。 下面是个例子:

class X {
    ...
private:
    void * _pMem1;
    UINT32 _pMemSize1;
    void * _pMem2;
    UINT32 _pMemSize2;

public:
    X& operator = ( const X & xo )
    {
        if( _pMem1 ) delete _pMem1;
        if( _pMem2 ) delete _pMem2;
        
        _pMem1 = new char[xo._pMemSize1];
        _pMem2 = new char[xo._pMemSize1];
        ...
    };
};

这里如果
 _pMem2 = new char[xo._pMemSize1]; Throw一个Exception,那么X只是被Copy了一半。 状态是不完整的。但是原来在pMem1&2中的数据已经消失了。如果是Strong Exception Safety,那么要求如果throw excpetion,那么class的数据应该恢复在之前的状态,比如经典的exception safe operator = 如下:

    X& operator = ( const X & xo )
    {
        X temp(xo);
        swap( *this, temp );
        return *this;
    };

swap是交换*this 和 Temp的所有数据。通常我们能够保证这个过程没有任何exception的产生。因此即使 temp(xo) throw一个exception, 也不会影响当前类的任何状态变化。


============ RAII ============

最后说一种不使用Exception而能保证没有Resource Leakage的技术。那就是 Resource Aquisition Is Initialization ( RAII ). 其原理很简单,就是C++标准保证一个被成功创建的 Object, 无论任何情况下(即使是在Throw exception ), 它的 Destructor都会被掉用。 因此,我们可以用一个object 的constructor 来获取资源,用Destructor来释放资源。下面举个最简单的应用,thread 的 asynchronization:

class CriticalSection {
public:
    CriticalSection( CRTICIAL_SECTION *pCs ) :
       _pCs(pCS)
    { EnterCriticalSection( _pCS ) };

    ~CriticalSection( )
    { LeaveCriticalSection( _pCS ) };

private:
   
 CRTICIAL_SECTION * _pCs;
};

通常我们使用Critical Section的时候,用下列方式:

void threadXX(
 CRTICIAL_SECTION * pCs)
{
   
 EnterCriticalSection( pCS );

    void * pTemp = new char[100000000];

    LeaveCriticalSection( pCS );
}

问题是如果
     void * pTemp = new char[100000000]; Throw一个 bad_alloc,那么 LeaveCriticalSection( pCS );就不会被掉用而直接返回,很容易导致死锁。类似的代码在游戏服务器端的设计是很常见的,正确的做法是使用上面定义的类:

void threadXX( CRTICIAL_SECTION * pCs)
{
   
 CriticalSection temp( pCS );

    void * pTemp = new char[100000000];
}

由于即使throw exception, C++保证temp的destructor一定会被调用。因此不会产生死锁的情况。

============ 其他 ============

比如下面的代码是很容易产生问题的:
    function( new char[100], new char[300] );
如果new char[300]throw exception,那么 new char[100]很有可能就不会被释放。

推荐使用auto_ptr或者boost中的Shared_ptr,特别是在class 的initialization list 中, 比如下列做法不使用catch exception也不会产生内存泄露:

class X{
    X() :
    _ptr1(new XXX()),
    _ptr2(new XXX())
    {};

private:
    auto_ptr<void *> _ptr1;
    auto_ptr<void *> _ptr2;
}

Destructor中不需要catch exception,因为destructor主要是调用其他的destructor,没有任何的destructor会throw exception的,所以没必要catch.

这次就说这么多,大家过的开心,下次见!

目 录 序言 前言 读者指南 第1章 引言 1 1.1 什么是设计模式 2 1.2 Smalltalk MVC中的设计模式 3 1.3 描述设计模式 4 1.4 设计模式的编目 5 1.5 组织编目 7 1.6 设计模式怎样解决设计问题 8 1.6.1 寻找合适的对象 8 1.6.2 决定对象的粒度 9 1.6.3 指定对象接口 9 1.6.4 描述对象的实现 10 1.6.5 运用复用机制 13 1.6.6 关联运行时刻和编译时刻的 结构 15 1.6.7 设计应支持变化 16 1.7 怎样选择设计模式 19 1.8 怎样使用设计模式 20 第2章 实例研究:设计一个文档编 辑器 22 2.1 设计问题 23 2.2 文档结构 23 2.2.1 递归组合 24 2.2.2 图元 25 2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种视感标准 32 2.5.1 对象创建的抽象 32 2.5.2 工厂和产品 33 2.5.3 Abstract Factory模式 35 2.6 支持多种窗口系统 35 2.6.1 我们是否可以使用Abstract Factory 模式 35 2.6.2 封装实现依赖关系 35 2.6.3 Window和WindowImp 37 2.6.4 Bridge 模式 40 2.7 用户操作 40 2.7.1 封装一个请求 41 2.7.2 Command 及其子 41 2.7.3 撤消和重做 42 2.7.4 命令历史记录 42 2.7.5 Command 模式 44 2.8 拼写检查和断字处理 44 2.8.1 访问分散的信息 44 2.8.2 封装访问和遍历 45 2.8.3 Iterator及其子 46 2.8.4 Iterator模式 48 2.8.5 遍历和遍历过程中的动作 48 2.8.6 封装分析 48 2.8.7 Visitor 及其子 51 2.8.8 Visitor 模式 52 2.9 小结 53 第3章 创建型模式 54 3.1 Abstract Factory(抽象工厂)— 对象创建型模式 57 3.2 Builder(生成器)—对象创建型 模式 63 3.3 Factory Method(工厂方法)— 对象创建型模式 70 3.4 Prototype(原型)—对象创建型 模式 87 3.5 Singleton(单件)—对象创建型 模式 84 3.6 创建型模式的讨论 89 第4章 结构型模式 91 4.1 Adapter(适配器)—对象结构型 模式 92 4.2 Bridge(桥接)—对象结构型 模式 100 4.3 Composite(组成)—对象结构型 模式 107 4.4 Decorator(装饰)—对象结构型 模式 115 4.5 FACADE(外观)—对象结构型 模式 121 4.6 Flyweight(享元)—对象结构型 模式 128 4.7 Proxy(代理)—对象结构型 模式 137 4.8 结构型模式的讨论 144 4.8.1 Adapter与Bridge 144 4.8.2 Composite、Decorator与Proxy 145 第5章 行为模式 147 5.1 CHAIN OF RESPONSIBIL ITY(职责链) —对象行为型模式 147 5.2 COMMAND(命令)—对象行为型 模式 154 5.3 INTERPRETER(解释器)—行为型 模式 162 5.4 ITERATOR(迭代器)—对象行为型 模式 171 5.5 MEDIATOR(中介者)—对象行为型 模式 181 5.6 MEMENTO(备忘录)—对象行为型 模式 188 5.7 OBSERVER(观察者)—对象行为型 模式 194 5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型 模式 208 5.10 TEMPLATE METHOD(模板方法) —行为型模式 214 5.11 VISIT
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值