1. 程式人生 > >C++基本功和 Design Pattern系列(4-6)

C++基本功和 Design Pattern系列(4-6)

======================================================
大家請把我的文章當參考,詳細內容 還請參照 權威書籍 
<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() { p = 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。當然,我們得到效率 的同時,失去的是可擴充套件性,良好的程式層次結構等等,大家要根據具體情況來權衡。

======================================================
 大家請把我的文章當參考,詳細內容  還請參照 權威書籍 
 <c++ programming language>如果文中有錯誤和遺漏,
 請指出,Aear會盡力更正, 謝謝!
======================================================

繼續上一章的內容,下面是經過調整後的Test Class程式碼:

class Test {
private:
    int internalData;
public:
    // constructor and destructor
    Test(int data = 0) : internalData(data) {};
    Test(const Test & Para) : internalData(Para.internalData) {};
    ~Test() {};
  
    // Operator overlording
    Test & operator += (const Test & Para1);
    Test operator + (const Test & Para1); 
};

Test & Test::operator += ( const Test & Para1 )
{
    internalData += Para1.internalData;
    return * this;
}

Test Test::operator + (const Test & Para1)
{
   return Test(*this) += Para1;
}

下面我們假設要給這個Test Class新增一種新的功能,讓Test Class 和 Integer之間能夠進行加法操作。 也就是說可以執行下列程式碼:

Test x1(10);
x1 = x1 + 5;
x1 += 5;

實際上,我們不需要進行任何修改,上面的程式碼就能夠正確執行。因為我們提供的建構函式Test(int data = 0) 能夠隱性的 (implicit type conversion) 把一個integer 轉換成一個Temporary Test Object,然後掉用Test Test::operator + (const Test & Para1)。因此,上面的程式碼等同於:

x1 = x1.operator + (Test(5));
x1 = x1.operator += (Test(5));

Implicit Type Conversion 實際上會帶來很多的麻煩,要想避免潛在的危險,最好在
Test(int data = 0)前面加上explicit,表示如果對interger轉換成Test,必須由程式設計師來控制,compiler不得進行隱性的操作。因此,要想似的 x1 = x1 + 5能夠正常執行,有2種方法:

x1 = x1 + static_cast<Test>(5);

x1 = x1 + Test(5);

還有一點需要注意的是,如果不用explicit type conversion,可以執行:

x1 = x1 + 5;

但是在編譯:

x1 = 5 + x1

的時候就會報錯了,除非使用一個Temporary Object ,如:

x1 = Test(5) + x1;

要想使Test Class 支援 x1 = 5 + x1,最好的方法就是用helper function. 下面我們來看看Operator的另外一中定義方式。

==================分割線
==================

我們可以使用friend function 來定義Test Class 的加法運算,程式碼如下:

class Test {
   Test(int data = 0) : internalData(data) {};
   ...
   // 針對這個Test Class, 並不需要下面這行。
   friend Test operator + ( const Test & Para1, const Test & Para2);
};

Test operator + ( const Test & Para1, const Test & Para2)
{
return Test(Para1) += Para2;
}

首先我們需要注意的是,Test(int data = 0)沒有用explicit,也就是說可以進行隱性的型別轉換,因此無論是執行:
x1 = x1 + 5;
還是:
x1 = 5 + x1;
都能夠編譯通過。

解決了基本的功能問題,讓我們繼續考慮一下效率。無論是在x1 = x1 + 5,還是在x1 = 5 + x1,都至少會掉用額外的constructor和destructor把5轉換成Test Object,這種浪費是很沒有必要的。其次允許compiler進行implicit type conversion並不是一個良好的習慣。解決這些問題的方法,就是提供專用的 operator + 來進行integer和Test object之間的加法操作,具體程式碼如下:

========== 支援 x1 + 5 ==========
Test operator + ( const Test & Para1, int Para2)
{
return Test(Para2) += Para1;
}

========== 支援 5 + x1 ==========
Test operator + ( int Para1, const Test & Para2 )
{
return Test(Para1) += Para2;
}

同時還要在class Test中加如下面2行(對於此例子並不需要,不過正常情況是需要的):

friend Test operator + ( int Para1, const Test & Para1 );
friend Test operator + ( const Test & Para1, int Para2 );

並且在constructor前面加上 explicit。當然,你也可以用Template進行定義,如下:

========== 支援 x1 + 5 ==========
template <class T>
T operator + ( const T & Para1, int Para2)
{
return T(Para2) += Para1;
}

實際上對於 template的定義,我個人並不推薦. 首先是因為namespace的問題,到底是global namespace呢?還是一個local namespace?如果是global namespace,那不一定所有的global class 都需要 operator +,這樣就提供了多餘的class操作。local namespace倒是可以用,前提是所有的class都定義了 +=. 也許對於大多數class來講,並不需要operator + 的操作。所以我覺得對於 operator 的定義,儘量少用 template (個人觀點).

==================分割線==================

下面說說關於型別轉換的operator. 對於一個Abstract Data Type來說,型別轉換是經常用到的,比如我們前面提到的從 integer轉換成 Test, 可以使用implicit type conversion 和 explicit type conversion. 但是如果我們想從Test 轉換成 integer,compiler無法支援自動的型別轉換,因此需要我們提供相應的operator:

class Test {
   ...
   // Type converstion from Test to int
operator int() { return internalData; };
}

那麼我們就可以執行:
int i = Test(10);

實際上,operator int()又是一種implicit type conversion,這並是收程式設計師的控制。良好的程式設計,是programmer能夠精確的控制每一個細微的操作。因此並不推薦使用 operator int(),好的方法是按照 < effective c++ > 中給出的那樣,提供一個asInt() method,來做explicti type conversion:

============ explicti ============
class Test {
   ...
   // Type converstion from Test to int
int asInt() { return internalData; };
}

================== Test++ & ++Test==================

相信大家都知道 Prefix ++ 和 Postfix ++的區別是什麼,下面是程式碼:

// Prefix
Test& operator++()
{
   
++internalData;
   return (*this);
}

// Postfix
Test operator++(int)
{
  
++*this;
   return --Test(*this); // 為了使用返回值優化,需要定義 --Test
}

我們只是簡單的看下效率問題,在 Prefix中也就是 ++ 返回的是reference,沒有temporary object,在 Postfix中返回的是個object,使用了Temporary。相信大家都知道了,能不使用 Test++的地方就不要使用,儘量使用 ++Test。

比如:

for( iterator i = XXX; XXX; ++i) // 不要使用 i++

對於postfix, compiler並不能保證肯定會優化成 prefix,所以寫程式碼的時候儘量注意。

================== 其他關於Operator==================

有些operator,並不推薦進行overload,因為會出現無法預料的情況。這些operator 包括:

&&,||, & , | , == , != , ","

舉個簡單的例子,如果你overload了",",那麼有一個for迴圈如下:

for( Test x1 = x2,i = 0; ; ) {....}

到底是x1 = x2 和 i = 0呢?還是 x1 = x2.operator , (i) = 0 呢?如果overload了 & ,對於邏輯判斷,x1 && x2,到底是 x1 && x2呢?還是 x1.operator & (&x2)呢?因此這些overload都會產生很多讓人費解的問題。

其次,很多operator overload需要很小心的對待,這些operator 如下:

new new[] delete delete[] -> [] ()

請仔細閱讀 C++ 標準,瞭解詳細內容後,再對這些operator進行overload,不然很容易造成程式的不穩定。

好了,關於operator 就說這麼多了,歡迎大家有空去我的Blog坐坐http://blog.sina.com.cn/u/1261532101下次見。

======================================================
 大家請把我的文章當參考,詳細內容  還請參照 權威書籍 
 <c++ programming language>如果文中有錯誤和遺漏,
 請指出,Aear會盡力更正, 謝謝!
======================================================

今天講的是 public inheritance, protected inheritance & private inheritance,內容不多,但是非常重要。基本的類的繼承,也就是inheritance的概念大家都清楚,明確的定義不再詳細說明了。先面舉個例子來說明:

class People {
   ...
   Walk();
   Eat();
};

class Student : public People{
  ...
  Study();
};

注意這行:
   class Student : public People {
中的public,表明是public inheritance,如果換成protected,就是protected inheritance, private就是private inhertance. 首先需要說明的是3種inheritance在語法上相似,但是在語意上完全不同。我們先從public inheritance說起。

=====================public inheritance
=====================

public inheritance最基本的概念就是"isa" ( is a )。 簡單的說,繼承類也就是Derived Class "is a" Base Class. 用上面的例子來說,People是base class, Student是 derived class,所以能夠推匯出: “student is a people” 這句話。如果你無法推匯出 "isa"的關係,那麼就不應該使用public inheritance.

其次,即使是能推匯出 "isa" 的關係,也必須滿足2個條件,才能使用 public inheritance. 這2個條件是:

   1. 所有Base Class的屬性,也就是 attribute,Derived Class都有。
   2. 所有Base Class的方法,Derived Class都應該包含。

在上面的例子中,student也是個people,所以能夠Walk() 和 Eat(),因此public inheritance 是合理的。
如果滿足 "isa" 但是不滿足上述條件,建議使用 Delegation/Composition,具體關於Delegation和Composition,在"C++基本功和 Design Pattern系列(1)" 中有說明。讓我們看下在《Effective C++》中的一個例子來說明這種情況:

class Rectangle {
   ...
   SetWidth();
   SetHeight();
};

class Square : public Rectangle {
   ...
   SetLength();
};

我們大家都知道,一個正方形Square,一定是一個長方形Rectangle,所以滿足"isa"的條件。我們給Rectangle提供了SetWidth()和SetHeight()的方法。如果不考慮上面2條,只考慮 "isa",那麼這個 public inheritance是合理的,但是讓我們看看會出現什麼問題。

在Square中我們要求長和寬必須相等,因此我們提供了SetLength(),來同時設定正方形的長和寬。但是有一位Bill小朋友無法分辨長方形和正方形,因此寫出瞭如下程式碼:

   Square MySquare;
   MySquare.SetWidth(100);
   MySquare.SetLenght(200);

那麼問題出現了,MySquare並不是一個Square。相信大家都明白了吧。語言的不精確性導致在設計過程中出現的錯誤是屢見不鮮的。因此,在public inheritance的時候要特別注意。也許有人會說,我們把SetHeight 和 SetWidth設定成Virtual然後在Square Class中過載不就可以了嗎? 如果Rectangle和Square 2個class都是你來寫,那麼也許不會出現問題。但是如果一個非常複雜的class,包含幾十個方法和幾十個屬性,並且由別人來寫,那麼你會不會仔細的閱讀程式碼並且overlord每一個需要的方法呢?即使你這樣做了,也許會帶來更多的麻煩。因為有可能破壞內部資料的一致性。

讓我們來看看interface inheritance的例子:

   Class Bird {
     ...
     virtual Fly() = 0;
   };

   Class Turkey : public Bird {
     ...
     Fly() { cout << "I cannt fly! Jessus....." <<endl; };
   };

   Turkey Bird0;
   ...
   Bird0.Flg();  // runtime error

首先,鳥能飛,這個沒有問題,火雞是一種鳥,這也沒有問題,但是: 火雞不能飛。問題出現了,client能夠呼叫Turkey的Fly()方法,但是得到的確是一個 RunTime Error! 這裡必須強調下:"RUNTIME ERROR!",對於遊戲程式來說,一個"RUNTIME ERROR"基本上就等於程式崩潰。和out of memory同等性質。如果你玩WOW做7個小時中間不能間斷的任務,然後出現一隻火雞給個RUNTIME ERROR....我想是人都會崩潰吧。

所以對於這種錯誤,我們要在編譯的時候儘量查出來,也就是 Prefer Compile Error over Runtime Error. 通過更改類的設計,我們可以避免類似的runtime error:
   Class Bird {
     ...
   };


   Class UnflyableBird : public Bird{
     ...
     // no fly() here
   };

   Class Turkey : publicUnflyableBird{
     ...
   };

   Turkey Bird0;
   ...
   Bird0.Flg();  // compile error....
  
所以,要想使用public inheritance,必須滿足:

   1. "ISA"
   2. 所有Base Class的屬性,也就是 attribute,Derived Class都有。
   3. 所有Base Class的方法,Derived Class都應該包含。


=====================private inheritance=====================

private inheritance和public inheritance最大的區別就在於,private inheritance不滿足"isa"的關係。舉個例子:

class People {
   ...
   Walk();
   Eat();
};

class ET: private People{
  ...
};

外星人ET是一種類似人的生物,能做一些類似人的動作,但是並不是人。從C++的語法上面來講,下面的程式碼是錯誤的:

   People* p = new ET();  // ERROR, ET is not a People

使用private inheritance的目的只是簡單的為了程式碼重用。因此如果不滿足public inheritance的條件,可以使用 Delegation/composition 和 Private Inheritance。 那麼在什麼情況下使用 private inheritance,什麼情況下使用
Delegation/Composition 呢?

有2種情況是推薦使用 private inheritance的,其他的情況下,推薦使用Delegation/Composition.

情況1: 需要對Base Class中的 private/protect virtual 進行過載。比如類似Draw() 等等。

情況2: 不希望一個Base class被 client使用。

關於情況2,舉個簡單的例子:
如果我們不希望Base Class被別人直接使用,有2種方法,第一是:把它設定成為abstract class, 也就是包含pure virtual function. 第2種方法是把constructor 和 descturctor設定成 protected.程式碼如下:

class Base {
protected:
  Base(); 
  virtual ~Base();
};

class Derived : private Base {
   ...
};

Base n; // Error, Base() cannot be called
Derived m; // ok, Derived can call Base()

這樣我們又可以保證n的程式碼可以被m使用,又可以防止 client直接呼叫 Base進行我們不希望的操作。

=====================protected inheritance=====================

protected inheritance和 private inheritance沒有本質的區別,但是如果我們希望的 Derived Class 能夠作為其他 class的基類,那麼就應該使用 protected inheritance.

今天就說這麼多,有空來我的Blog做客: http://blog.sina.com.cn/u/1261532101 ,下次見!