C++基本功和 Design Pattern系列(1-3) - Inheritance VS Delegation

本文探讨了C++中const和引用的有效使用方法,对比了继承与委托的区别,并深入讲解了四种类型转换方式。此外,还讨论了构造函数、析构函数的最佳实践,以及接口继承与实现继承的不同应用场景。
首先恭喜C++版开张,这样Aear又多了一个可以发表废话的地方。基本上C++基本功系列会是一个比较长的东西,因为C++和OO的内容太多,太难说的 清楚。Aear本人也不能保证所有的说的都正确,但是能发上来的内容都参照过  ISO,  <<The  C++   programming  language>>,以及其他的参考资料。

其 次,为什么Aear继续写基本语言的东西? Aear本来想写一些DirectX啊,shader啊,game开发理论什么的。不过想来想去,还是觉得不 要误导大家的好。主要原因是,看aear文章的大部分都是非专业人士,想走进游戏开发这个领域。根据Aear的个人经验,游戏开发最重要的是基本功,而不 是DirectX和OpenGL这些API的使用方法。下面列出的是我认为对游戏开发比较有用的东西,从重要到不重要排列。
       1. 英语 (没办法,几乎所有用的文档,都是英文的,不过也是最可以忽略的)
       2. 基础物理学和数学 (这个不用我说了吧)
       3. C/C++和其他程序设计语言 (这个没的说,基本工,至少1到3年的时间能有小成)
       4. 数据结构,算法理论 (几乎游戏开发天天用)
       ===========这里往上是基础,Aear认为是进入这个行业的必然条件=========
       5. 计算机图形学,计算机网络
       6. 高等数学,空间几何,微积分等等
       7. Windows 体系结构,计算机结构,软件工程理论
       8. 开发环境
       9. API (DirectX OpenGL)等

5到7一般是大学计算机专业的必学内容,如果能够有扎实的1到8的基础,DirectX和OpenGL应该花1个星期学就够了,1个月就能精通。

根据Aear的面试经验,程序设计50%,其中30%数据结构内容,从基本链表到高等图论内容全考。 20%数学基础,10%优化,其他还有一些算法分析等等。基本上别人问都不问DirectX和OpenGL,当然也是我拿了Demo去的缘故。还有我的经验只做参考,请大家根据自己的实际情况来决定学习内容和面试准备。下面让我们顺便看看Blizzard对于程序员的要求:

Requirements

    Strong C/C++ and PC programming skills.
    Minimum of years experience programming at least one title that has already shipped.
    passion for games and game development.
    Good communication skills.
    Bachelor's degree in Computer Science or related field. 

Plusses

    Graphics or sound programming experience.
    Strong math background.
    Experience in game design.
    Knowledge of Win32 and DirectX API's.
    Prior work experience on an MMORPG. 

看到没,DirectX排倒数第2,还是plus里的,不会根本无伤大雅。很多公司都和blizzard差不多,只要你基本工扎实,学DirectX很快的。

=====================分割线=====================
好了,废话不多说,让我们开始吧。首先是const 和 reference的使用,这部分内容已经在C语言里说过,但是在C++里又有了一些扩展。 

C++允许使用object作为参数传递,但是object有大有小。 比如下面一个object class:

class CBitmap{
public:
    CBitmap();
    ~CBitmap();
private:
    const static UINT32 MAX_BUFFER_SIZE 65536;
    UINT32 m_Height;
    UINT32 m_Width;
    BYTE    m_Buffer[MAX_BUFFER_SIZE];
};

如果我们有个函数,是DrawBitmap,那么就有两种不同的声明方式。
========错误的方式========
void DrawBitmap(CBitmap Bitmap);
========正确的方式========
void DrawBitmap(CBitmap Bitmap);
如 果我们使用第一种方式,那么程序就会创建一个临时的CBitmap object,然后把Bitmap拷贝进去,传送给DrawBitmap。这可不是只 会拷贝几个字节那么简单,而且CBitmap的所有内容,包括里边的m_Buffer都会拷贝。如果使用reference也就是 "&",就不 会有任何操作。如果在DrawBitmap里边不会改变Bitmap的任何状态,也就是不会改变任何属性,那么就最好加上const关键字,最后的 DrawBitmap的声明应该是:
void DrawBitmap(const CBitmap Bitmap); 

这个时候,在DrawBitmap里,只能调用CBitmap中声明为const的函数。让我们来看看代码

class CBitmap{
public:
    CBitmap();
    ~CBitmap();
    UINT32 GetHeight(void) const;
    void SetHeight(void);
private:
    const static UINT32 MAX_BUFFER_SIZE 65536;
    UINT32 m_Height;
    UINT32 m_Width;
    BYTE    m_Buffer[MAX_BUFFER_SIZE];
};

UINT32 CBitmap::GetHeight(void) const
{
   return m_Height;
}

void CBitmap::SetHeight(UINT32 Height)
{
  m_Height Height;
}

大 家看到了,在 UINT32 GetHeight(void) const; 有个const,意思是这个函数不会改变任何CBitmap里的属性值。由 于SetHeight()会改变m_Height,所以不能声明为const. 在DrawBitmap里边,由于参数是const类型,所以只能调用 const的方法。
void DrawBitmap(const CBitmap Bitmap)
{
    Bitmap.GetHeight();        // 正确,没有问题
    Bitmap.SetHeight(100);   // 错误,Bitmap是const类型
}

值得注意的是,尽量把class的声明中,不改变属性的方法,声明为 const ,这就是所谓的良好的程序风格。

最后,如果在CBitmap里边有另外一个类,比如是CNormalMap,那么如果有个方法用来取得CNormalMap,code如下

class CBitmap {
.....
// 省略
Private: 
    CNormalMap NormalMap;
public: 
    CNormalMap GetNormalMap(void);
}

这 是一种类做法,但是并没有充分的考虑效率。首先,返回的CNormalMap不是引用,这个是正确的做法,对象拷贝以后,即使改变内容,也不会影响 Class的状态。但是如果我们本来就不打算改变CNormalMap的状态呢?那么这个函数的调用效率就低下了,所以我们一般提供2个函数,代码如下:

class CBitmap {
.....
// 省略
Private: 
    CNormalMap NormalMap;
public: 
    CNormalMap GetNormalMap(void);
    const CNormalMap GetStaticNormalMap(void);
}
或者利用C++的函数重载,做如下声明:
class CBitmap {
.....
// 省略
Private: 
    CNormalMap NormalMap;
public: 
    CNormalMap GetNormalMap(void);
    const CNormalMap GetNormalMap(void) const;
}

如果我们不打算改变NormalMap的状态,那么就掉用GetStaticNormalMap() 或者 GetNormalMap() 的const调用,这样我们可以充分的利用reference的效率。

===============================================
C+ +真是内容多呀,一个const和reference就讲了一大堆。好了, 继续今天Design Pattern的内容。所谓 Design Pattern,翻译过来就是设计模式,是OO语言的一些基本运用。Aear会讲一些Design Pattern,并且给出在游戏中的可 能的运用方式。今天第一课将会介绍Design Pattern中的两个基本概念,Inheritance 和 Delegation.

所谓Inheritance就是继承,我想学过C++的人都知道什么是继承。以上面的CBitmap为例子,如果我们想生成一个CTexture类,并且保留CBitmap的功能,比如GetBitmapHeight什么的,可以这么做:

class CTexture : public CBitmap {
public:
    CTexture();
    ~CTexture();
};

当时还有另外一种方法,并不使用继承,而是把CBitmap当做CTexture的一个成员,这就是Delegation。代码如下:

class CTexture {
public:
    CTexture();
    ~CTexture();
private:
    CBitmap InternalBitmap;
public:
    UINT32 GetHeight(void)   return InternalBitmap.GetBitmapHeight();  };    
};

关 于Inheritance和Delegation哪个更好,Aear不想在这里说,因为网上已经有太多的关于这个争论的文章。但是Aear的个人观点是能 用Delegation的地方,就不要使用Inheritance。道理很简单,不同class层次的函数调用,很容易使程序员产生混乱。

举 个简单的例子: 比如CTexture从CBitmap继承了GetBitmapHeight 方法,但是CTexture又提供了Bitmap的缩小功 能,或者是mipmap,所以提供了一个函数 GetTextureHeight()。 只有GetTextureHeight能返回正确的 texture size的内容。 然后一个不明就里的程序员使用这个类,他发现了GetBitmapHeight这个函数,想当然的觉得是这个函数是用 来取得texture的大小,那么。。。。。一切都错乱了。

所以在尽可能的情况下使用Delegation,在其他情况,比如一些Design Pattern和Interface的时候,使用Inheritance (纯个人观点).

好了,今天就说这么多了。作为开场第一章内容也够了吧。大家下次见。


C++基本功和 Design Pattern系列(2) Type Cast, Interface Inheritance VS Implementation Inheritance
====================================================================

又到周末了,Aear在此感谢大家坐这么近来听我说书。今天讲的是C++的类型转换(比较无聊的内容,但最好看看,因为可以帮助大家减少程序中的bug)。许多学过C的朋友一定还记得C语言中的类型转换,例如:

float FloatNum 1.234;
int IntNum (int)FloatNum;
// IntNum 1

这是比较正常的类型转换,稍微危险一点的转换如下:

float FloatNum 1.234;
float pFloatPointer &FloatNum;
int pIntPointer (int *)pFloatPointer;
// *pIntPointer 里边就是乱七八糟的东西了

C 的类型转换虽然很方便,但是却带来了更多的问题。比如 C的类型转换可以允许你进行任意类型之间的转换。 非常随意的使用类型转换很容易造成程序逻辑的混 乱,使人看不懂你写的代码,或者编译器不能正确识别你的转换的意图,所以做出错误的转换方式。其次,C类型的转换很难查错。特别是在大型的工程中,你想找 出一个因为 (uint)转换成 int而产生益出的问题,可能需要查看上千行包含"(int)"的代码。为了避免诸如此类情况的发生 C++引入了4种 类型转换的方式。请记住,C的类型转换只是为C语言设计的,并不适合C++。C++支持C的类型转换只不过是为了向下兼容的考虑。

让我们来看看C++的4种类型转换(关于这4种类型转换的详细说明,请参见<C++ Programming Language> 3rd Edition):

static_cast<>()
dynamic_cast<>()
const_cast<>()
reinterpret_cast<>()

我们一个一个来说
================== static_cast ==================
static_cast<>() 
static_cast可以用来进行相关类型之见的转换,比如double 转成 float, float转成 int 以及 有关联的pointer之间,有关联的 class pointer 之间的转换。

========比如========

float FloatNum 1.234;
int IntNum static_cast<int>(FloatNum);    // IntNum 1;

========或者========

class BaseClass{
public:
    BaseClass();
    virtual ~BaseClass();
};

class DerivedClass public BaseClass{
public:
    DerivedClass();
    ~DerivedClass();
    void DoSomething(void);
};

BaseClass pBaseClass new BaseClass();
// 没问题,不过call pDerviedCalss->DoSomething()很有可能会crash
DerivedClass pDerivedClass static_cast<DerivedClass *>pBaseClass;

DerivedClass pDerivedClass new DerivedClass();
// 没问题,很安全
BaseClass pBaseClass static_cast<BaseClass *>(pDerivedClass);

值 得注意的是 static_cast是在程序编译的时候检查类型转换是否符合要求,并不在程序运行期间进行检查,因为没有 runtime overhead, 对速度的影响比较小。所以在你对类型转换很有把握的时候,可以尽量的使用static_cast。另外  static_cast在转换指针的时候并不能保证转换前的指针地址和转换后的指针地址相同,特别是在多重继承的类结构中,指针地址经常会变化。

下面是一些通常比较危险的类型转换,

========包括========
unsigned 转 sign 比如 uint 转成 int
double 转 float
long 转 int (64位操作系统有危险,32位无)

这些转换都是值域大的转成值域小的数,或者无符号转成有符号。比如:

unsigned int 4294967290;
int static_cast<int>(k);

这里k已经超出了int的值域,所以 m的最后结果是 -6。所以在做以上的类型转换的时候,要特别的小心。


================== dynamic_cast ==================
dynamic_cast 是用来对相关的class 指针之间进行的类型转换。由于dynamic_cast在运行过程中对转换进行安全性检查,所以在很大程度上影响程序的运行速 度,并且在便宜的时候需要打开runtime type info 的开关 /GR,所以并不推荐使用。

如果要使用dynamic_cast那么要求转换的类至少需要含一个虚函数,并且只能对类的指针进行转换操作,指针不包括 void *。 例如:

class BaseClass{
public:
    BaseClass();
    virtual ~BaseClass();
    virtual void DoSomething(void);
};

class DerivedClass public BaseClass{
public:
    DerivedClass();
    ~DerivedClass();
    void DoSomething(void);
};

DerivedClass pDerivedClass new DerivedClass();
// 没问题
BaseClass pBaseClass dynamic_cast<DerivedClass *>(pDerivedClass);

BaseClass pBaseClass new BaseCalss();
// 有问题,基类转成派生类,dynamic_cast会返回null 所以 pDerivedClass == NULL
DerivedClass pDerivedCalss dynamic_cast<DerivedClass *>(pBaseClass);

================== const_cast ==================
顾名思义,就是把const变量转换成 non-const变量,或者把volatile转成non-volatile。这是最不推荐使用的一种转换,只有在特殊的情况下才会使用。如果你需要大量的使用const_cast,那么只能说明程序中存在着设计缺陷。

const_cast的使用如下:
float FloatNum 12;
const float pConstFloatPointer &FloatNum;
// ok 这么用没问题
float pFloatPointer const_cast<float *>(pConstFloatPointer);
// 编译错误,const_cast只能进行const和non-const之间的转换
int pFloatPointer const_cast<int *>(pConstFloatPointer);

================== reinterpret_cast ==================
reinterpret_cast是最危险的一种转换类型,它并不对数据进行任何实际的转换操作,而是直接把数据当作另一种类型来使用(跟内存拷贝的功能差不多)。比如:

class ClassX{...};
class ClassY{...};

ClassX pClassX new ClassX();
// 没问题,不过除非classX和classY都是结构相同的interface,否则并不安全,程序很容易crash
ClassY pClassY reinterpret_cast<ClassY *>(pClassX);

reinterpet_cast比较有用的地方就是函数指针的转换,比如把一个指针存到一个函数指针数组中:

      typedef void (*FuncPtr)();
      FuncPtr FuncPtrArray[10];
      int DoSomething() {...};
      // 这里使用reinterpret_cast
      FuncPtrArray[0] reinterpret_cast<FuncPtr>(&DoSomething);

总的来说,尽可能的使用这4种转换来清晰的表达你转换的目的,尽量不要使用C风格的转换。

================== Design Pattern ==================
今天的Design Pattern讲讲另外两个概念: Interface Inheritance 和 Implementation Inheritance。

上 次Aear已经介绍了Inheritance 和 Delegation,并且说如果能用Delegation的地方,就最好不要使用 Inheritance。但是如果必须使用Inheritance怎么办?因此就出现了不同使用Inheritance(继承)的方式。第一种就是  Interface Inheritance。

简单的说,Interface Inheritance就定义一个 abstract class (抽象类),把所有提供的类方法都在这个抽象类中定义成纯虚函数,然后在派生类中实现这些虚函数。对于这个 abstract class,我们可以称它为: Interface。 (是不是有点象 COM?)。 比如在游戏中,所有的敌人都可以移动和攻击,但 是不同的敌人类型,攻击和移动的具体方式都不一样。我们就可以用一个抽象类在表示所有的敌人,在派生类中具体实现不同移动和攻击方式。
下面让我们来看看代码:

class Enemy{
public:
    virtual void Move(void) 0;
    virtual void Attack(void) 0;
};

// 人类敌人
class HumanEnemy public Enemy{
public:
    void Move(void) {...}; // 用2条腿走路
    void Attack(void) {...}; // 用拳头或武器攻击
};

// 怪物敌人
class MonsterEnemy public Enemy{
public:
    void Move(void) {...}; // 用4跳腿走路
    void Attack(void) {...}; // 用牙齿和爪子攻击
};

在运行AI的时候我们不需要具体判断对方是人类还是敌人,直接调用 move和attack就行了,代码如下:

void CalculateAction( Enemy pEnemy)
{
    pEnemy->Move();
    pEnemy->Attack();
}

main()
{
   ....
   HumanEnemy pHumanEnemy new HumanEnemy();
   MonsterEnemy pMonsterEnemy new MonsterEnemy();
   CalculateAction(static_cast<Enemy *>pHumanEnemy);
   CalculateAction(static_cast<Enemy *>pMonsterEnemy);
   ....
}

=========== 小分割线 ===========

关 于implementation inheritance,就是Aear在上一章里举的关于CBitmap和CTexture的例子。其中CBitmap 只基类,提供了 GetBitmapHeight()方法。 CTexture是CBitmap的派生类,提供了GetTextureHeight()的 方法。这种结构很容易使人产生程序逻辑结构的混乱,并不推荐使用。Aear的个人观点是:
    能使用 Delegation就用
    不能用Delegation就用 Interface Inheritance
    用不了Interface Inheritance就重新设计你的类结构,使之能使用前两种方式
    如果重新设计系统或者使用Interface Inheritance开销太大,再用Implementation Inheritance。

其 实所有的Implementation Inheritance都可以通过Interface Inheritance来实现,具体来说,就是定义一个 abstract class基类,在此基础上,定义派生类的abstract class (派生类也是interface),如此定义下去,直到所有 需要实现的类都拥有自己的interface class为止。

好了就说这么多了,大家有空去我的blog坐坐Aear's Blog,下次见!



======================================================
 大家请把我的文章当参考,详细内容  还请参照 权威书籍 
 <c++ programming language>如果文中有错误和遗漏,
 请指出,Aear会尽力更正, 谢谢!
======================================================
最 近实在是太忙了,无工夫写呀。只能慢慢来了。呵呵,今天Aear讲的是class.ctor 也就是constructor, 和  class.dtor, destructor. 相信大家都知道constructor 和 destructor是做什么用的,基本功能我就不废话 了。下面先说效率的问题,让我们看个简单的例子:

class SomeClass;   // forward declaration

class AnotherClass {
private:
    SomeClass SomeClassInstance;
public:
    AnotherClass(const SomeClass Para) SomeClassInstance Para; };
    ~AnotherClass();
};

也许这是很多初学者经常写出来的代码,Aear以前也写过。让我们来看看这段代码有什么问题。

首 先需要说明的是,在一个class实例化之前,所有的member都会被初始化,如果member是个class,那么那个class的 constructor就会被调用。也就是说,在运行AnotherClass的constructor之前,SomeClass的 constructor就已经运行了。接下来的代码里,SomeClassInstance又被重新执行了次 操作。也就是说,我们在给 SomeClassInstance附初值的时候,调用了2次SomeClass的method. 这个浪费也太大了,比较标准的方式是使用初始化列表, 如下:

    AnotherClass (const SomeClass Para): SomeClassInstance(Para) {};
如果有多个类成员,可以用","来分割,如:

    AnotherClass (const SomeClass Para1, UINT32 Para2):
                   SomeClassInstance(Para1),
                   SecondAttr(Para2),
                   ThirdAttr(Para3) {};
值 得注意的是, 类成员的初始化顺序和在类中的声明顺序应该一致。这个是有compiler来控制的,并不根据你在AnotherClass的 constructor中提供的初始化顺序来进行。所以,如果你想先初始化ThirdAttr,然后把ThirdAttr传到SecondAttr作为初 始化参数,是会失败的。只有改变声明顺序才会成功。

同理,在声明类变量被附初值的时候,使用拷贝构造函数,效率更高:

=====错误=====
class x1;
x1 x2;

=====正确=====
class x1(x2);

===================分割线===================

从 上面的例子可以看到,几乎所有的class,都需要提供拷贝构造函数,也就是 className(const className &)。同时 值得注意的是,如果提供了拷贝构造函数,一般也就需要提供 "="操作,也就是 className operator  (const className &),说到 operator =, 也有必要强调下implicit type conversion的问 题,这将会在以后的章节张有详细描述。至于为什么要提供 operator =,举个简单的例子:
 
class1 {
public:
    class1() new int[100]; };
    ~class1() delete[] p; };
private:
    char* p;
x1, x2;

如 果class1不提供operator =, 那么运行 x1 x2的时候,C++会运行最基本的拷贝操作,也就是 x1.p x2.p,那么在 x1被释放的时候,delete p;被执行。这时候 x2再要访问p,p已经变成非法指针了。 也许有人会说,我才不会用x1 x2这么危险的操 作,那让我们看看更加隐性的操作吧,例子如下:

void func(class1 Para) {...};

func(x1);

这 时候,c++会调用class1的拷贝构造函数,来把参数从x1里拷贝到Para,如果class1没有提供copy constructor,那么c+ +就执行简单拷贝工作,也就是 Para.p x1。当func返回的时候,Para被释放,调用 Para.~class1(),并且 delete p;那么x1.p就变成非法指针了。

这样大家就知道为什么要同时提供copy constructor和 operator =了吧。特别是在class里有指针的情况下,必须提供以上2个method。如果不想提供,可以把他们设为private,代码如下:

class1 {
...
private:
    class1 (const class1 &);
    class1 operator (const class1 &);
}
这样别人在执行 和 func()的时候就会报错了。

还有,在声明构造函数的时候,单参数的构造函数,最好都用explicit来声明,例如:

class1 {
public:
    class1(int Para) {...}
    ...
};

其中class1(int Para)是个单参数的构造函数,如果执行下列操作,如:

class1 x1 2;

的 时候,因为2不是class1,所以c++会用隐性的类型转换,也就是把2转换成class1,因此会调用class1(2),然后用operator  = 符值给 x1. 这种操作经常会产生很多问题。比如如果我们提供了 operator == ,那么 在 if(x1 == 2)的时候,c++也会 进行类似的操作,可能会产生我们不需要的结果。所以,对于这种单参数的constructor 最好做如下声明:

explicit class1 (int Para) {...}

这样做再执行 class1 x1 2;的时候就会报错了,explicit的意思就是C++ 的compiler不能做隐性类型转换,必须由程序员做type cast,比如:

class1 x1 static_cast<class1>(2) 才会成功。

===================分割线===================
在运行constructor的时候,值得注意的一点就是,如果在constructor里,要初始化会throw exception的代码,一定要在constructor里catch。比如:

class1 {
    class1()
    {
       pInt new int[100];
       try {
           pClass2 new pClass2;
       }catch(...)
       delete pInt; throw; };
     }
}

大家看的明白了吧,如果不catch pClass2的exception,pInt分配的内存就不会释放,因为constructor如果失败,c++是不会调用destructor的。

===================分割线===================
最后关于destructor,需要注意的是,如果是被继承的base class,destructor一定要是virtual。比如:

BaseClass ()
{
public:
    BaseClass();
    virtual ~BaseClass();
}

DerivedClass public BaseClass()
{
public:
    DerivedClass();
    ~DerivedClass();
}

BaseClass pBase static_cast<BaseClass *>(new DerivedClass());
delete pBase;

如果BaseClass的destructor是virtual,那么正确的ctor dtor调用顺序是:

BaseClass();
DerivedClass();
~DerivedClass();
~BaseClass();

如果不是Virtual,调用顺序是:

BaseClass();
DerivedClass();
~BaseClass();

也 就是说,DerivedClass的派生类不能被正确调用,这主要是因为在delete的时候c++并不知道你delete的是  DerivedClass, 因此需要把BaseClass的 dtor 设置成 virtual, 这样可以使用 vptr在 vtbl中查找  destructor,从而能够正确的调用destructor。

===================分割线===================
从上面的例子大家也看出来了,如果是派生类,那么就要调用基类的constructor,在多层次的派生类创建过程中,所以基类的constructor都要被调用。 destructor同理。因此要想提高效率,可以在关键代码短使用非派生类。

也 许有人会说,所有的constructor和destructor都被compiler inline了,但是即使是inline并且 base class的constructor中不进行任何操作,c++也要为每个类设置vptr,也是有不需要的overhead。当然,我们得到效率 的同时,失去的是可扩展性,良好的程序层次结构等等,大家要根据具体情况来权衡。


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值