C++基本功和 Design Pattern系列(4-6)
大家請把我的文章當參考,詳細內容 還請參照 權威書籍
<c++ programming language>如果文中有錯誤和遺漏,
請指出,Aear會盡力更正, 謝謝!
======================================================
最 近實在是太忙了,無工夫寫呀。只能慢慢來了。呵呵,今天Aear講的是class.ctor 也就是constructor, 和 class.dtor, destructor. 相信大家都知道constructor
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 ,下次見!