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.

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

你可能感兴趣的:(类,异常)