最近實在是太忙了,無工夫寫呀。只能慢慢來了。呵呵,今天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。當然,我們得到效率的同時,失去的是可擴充性,良好的程式階層等等,大家要根據具體情況來權衡。