高質量C++/C程式設計指南(第9章 類的建構函式、解構函式與賦值函式) (轉)

worldblog發表於2007-12-12
高質量C++/C程式設計指南(第9章 類的建構函式、解構函式與賦值函式) (轉)[@more@] 

章 類的構造、解構函式與賦值函式

建構函式、解構函式與賦值函式是每個類最基本的函式。它們太普通以致讓人容易麻痺大意,其實這些貌似簡單的函式就象沒有頂蓋的下水道那樣危險。

  每個類只有一個解構函式和一個賦值函式,但可以有多個建構函式(包含一個複製建構函式,其它的稱為普通建構函式)。對於任意一個類A,如果不想編寫上述函式,C++將自動為A產生四個預設的函式,如

  A(void);    // 預設的無引數建構函式:namespace prefix = o ns = "urn:schemas--com::office" />

  A(const A &a);    // 預設的複製建構函式

  ~A(void);    // 預設的解構函式

  A & operate =(const A &a);  // 預設的賦值函式

 

這不禁讓人疑惑,既然能自動生成函式,為什麼還要員編寫?

原因如下:

(1)如果使用“預設的無引數建構函式”和“預設的解構函式”,等於放棄了自主“初始化”和“清除”的機會,C++發明人Stroustrup的好心好意白費了。

(2)“預設的複製建構函式”和“預設的賦值函式”均採用“位複製”而非“值複製”的方式來實現,倘若類中含有指標變數,這兩個函式註定將出錯。

 

對於那些沒有吃夠苦頭的C++程式設計師,如果他說編寫建構函式、解構函式與賦值函式很容易,可以不用動腦筋,表明他的認識還比較膚淺,水平有待於提高。

本章以類String的設計與實現為例,深入闡述被很多教科書忽視了的道理。String的結構如下:

  class String

  {

    public:

    String(const char *str = NULL);  // 普通建構函式

    String(const String &other);  // 複製建構函式

  ~ String(void);      // 解構函式

    String & operate =(const String &other);  // 賦值函式

    private:

  char    *m_data;        // 用於儲存字串

  };

建構函式與解構函式的起源

  作為比C更先進的語言,C++提供了更好的機制來增強程式的性。C++編譯器具有嚴格的型別安全檢查功能,它幾乎能找出程式中所有的語法問題,這的確幫了程式設計師的大忙。但是程式透過了編譯檢查並不表示錯誤已經不存在了,在“錯誤”的大家庭裡,“語法錯誤”的地位只能算是小弟弟。級別高的錯誤通常隱藏得很深,就象狡猾的罪犯,想逮住他可不容易。

  根據,不少難以察覺的程式錯誤是由於變數沒有被正確初始化或清除造成的,而初始化和清除工作很容易被人遺忘。Stroustrup在設計C++語言時充分考慮了這個問題並很好地予以解決:把的初始化工作放在建構函式中,把清除工作放在解構函式中。當物件被建立時,建構函式被自動。當物件消亡時,解構函式被自動執行。這下就不用擔心忘了物件的初始化和清除工作。

  建構函式與解構函式的名字不能隨便起,必須讓編譯器認得出才可以被自動執行。Stroustrup的命名方法既簡單又合理:讓建構函式、解構函式與類同名,由於解構函式的目的與建構函式的相反,就加字首‘~’以示區別。

除了名字外,建構函式與解構函式的另一個特別之處是沒有返回值型別,這與返回值型別為void的函式不同。建構函式與解構函式的使命非常明確,就象出生與死亡,光溜溜地來光溜溜地去。如果它們有返回值型別,那麼編譯器將不知所措。為了防止節外生枝,乾脆規定沒有返回值型別。(以上典故參考了文獻[Eekel, p55-p56])

建構函式的初始化表

  建構函式有個特殊的初始化方式叫“初始化表”(簡稱初始化表)。初始化表位於函式參數列之後,卻在函式體 {} 之前。這說明該表裡的初始化工作發生在函式體內的任何程式碼被執行之前。

  建構函式初始化表的使用規則:

u  如果類存在繼承關係,派生類必須在其初始化表裡基類的建構函式。

例如

  class A

  {…

  A(int x);  // A的建構函式

}; 

  class B : public A

  {…

  B(int x, int y);// B的建構函式

  };

  B::B(int x, int y)

   : A(x)       // 在初始化表裡呼叫A的建構函式

  {

    …

u  類的const常量只能在初始化表裡被初始化,因為它不能在函式體內用賦值的方式來初始化(參見5.4節)。

u  類的資料成員的初始化可以採用初始化表或函式體內賦值兩種方式,這兩種方式的不完全相同。

  非內部資料型別的成員物件應當採用第一種方式初始化,以獲取更高的效率。例如

  class A

{…

  A(void);      // 無引數建構函式

  A(const A &other);    // 複製建構函式

  A & operate =( const A &other);  // 賦值函式

};

 

  class B

  {

    public:

    B(const A &a);  // B的建構函式

    private: 

  A  m_a;    // 成員物件

};

 

示例9-2(a)中,類B的建構函式在其初始化表裡呼叫了類A的複製建構函式,從而將成員物件m_a初始化。

示例9-2 (b)中,類B的建構函式在函式體內用賦值的方式將成員物件m_a初始化。我們看到的只是一條賦值語句,但實際上B的建構函式幹了兩件事:先暗地裡建立m_a物件(呼叫了A的無引數建構函式),再呼叫類A的賦值函式,將引數a賦給m_a。

 

B::B(const A &a)

 : m_a(a) 

{

  …

}

B::B(const A &a)

{

m_a = a;

}

 示例9-2(a) 成員物件在初始化表中被初始化  示例9-2(b) 成員物件在函式體內被初始化

 

對於內部資料型別的資料成員而言,兩種初始化方式的效率幾乎沒有區別,但後者的程式版式似乎更清晰些。若類F的宣告如下:

class F

{

  public:

  F(int x, int y);    // 建構函式

  private:

  int m_x, m_y;

  int m_i, m_j;

}

示例9-2(c)中F的建構函式採用了第一種初始化方式,示例9-2(d)中F的建構函式採用了第二種初始化方式。

 

F::F(int x, int y)

 : m_x(x), m_y(y) 

{

  m_i = 0;

  m_j = 0;

}

F::F(int x, int y)

{

  m_x = x;

  m_y = y;

  m_i = 0;

  m_j = 0;

}

 示例9-2(c) 資料成員在初始化表中被初始化  示例9-2(d) 資料成員在函式體內被初始化

構造和析構的次序

  構造從類層次的最根處開始,在每一層中,首先呼叫基類的建構函式,然後呼叫成員物件的建構函式。析構則嚴格按照與構造相反的次序執行,該次序是唯一的,否則編譯器將無法自動執行析構過程。

一個有趣的現象是,成員物件初始化的次序完全不受它們在初始化表中次序的影響,只由成員物件在類中宣告的次序決定。這是因為類的宣告是唯一的,而類的建構函式可以有多個,因此會有多個不同次序的初始化表。如果成員物件按照初始化表的次序進行構造,這將導致解構函式無法得到唯一的逆序。[Eckel, p260-261]

示例:類String的建構函式與解構函式

  // String的普通建構函式

  String::String(const char *str)

{

  if(str==NULL)

  {

    m_data = new char[1];

    *m_data = ‘’;

  } 

  else

  {

    int length = strlen(str);

    m_data = new char[length+1];

    strcpy(m_data, str);

  }

 

// String的解構函式

  String::~String(void)

{

  delete [] m_data;

// 由於m_data是內部資料型別,也可以寫成 delete m_data;

  }

不要輕視複製建構函式與賦值函式

  由於並非所有的物件都會使用複製建構函式和賦值函式,程式設計師可能對這兩個函式有些輕視。請先記住以下的警告,在閱讀正文時就會多心:

u  本章開頭講過,如果不主動編寫複製建構函式和賦值函式,編譯器將以“位複製”的方式自動生成預設的函式。倘若類中含有指標變數,那麼這兩個預設的函式就隱含了錯誤。以類String的兩個物件a,b為例,假設a.m_data的內容為“hello”,b.m_data的內容為“world”。

現將a賦給b,預設賦值函式的“位複製”意味著執行b.m_data = a.m_data。這將造成三個錯誤:一是b.m_data原有的沒被釋放,造成記憶體洩露;二是b.m_data和a.m_data指向同一塊記憶體,a或b任何一方變動都會影響另一方;三是在物件被析構時,m_data被釋放了兩次。

 

u  複製建構函式和賦值函式非常容易混淆,常導致錯寫、錯用。複製建構函式是在物件被建立時呼叫的,而賦值函式只能被已經存在了的物件呼叫。以下程式中,第三個語句和第四個語句很相似,你分得清楚哪個呼叫了複製建構函式,哪個呼叫了賦值函式嗎?

String  a(“hello”);

String  b(“world”);

String  c = a;  // 呼叫了複製建構函式,最好寫成 c(a);

c = b; // 呼叫了賦值函式

本例中第三個語句的風格較差,宜改寫成String c(a) 以區別於第四個語句。

示例:類String的複製建構函式與賦值函式

  // 複製建構函式

  String::String(const String &other)

  { 

// 允許操作other的私有成員m_data

  int length = strlen(other.m_data);

  m_data = new char[length+1];

  strcpy(m_data, other.m_data);

}

 

// 賦值函式

  String & String::operate =(const String &other)

  { 

  // (1) 檢查自賦值

    if(this == &other)

    return *this;

 

  // (2) 釋放原有的記憶體資源

    delete [] m_data;

 

  // (3)分配新的記憶體資源,並複製內容

  int length = strlen(other.m_data);

  m_data = new char[length+1];

    strcpy(m_data, other.m_data);

 

  // (4)返回本物件的引用

    return *this;

 

  類String複製建構函式與普通建構函式(參見9.4節)的區別是:在函式入口處無需與NULL進行比較,這是因為“引用”不可能是NULL,而“指標”可以為NULL。

  類String的賦值函式比建構函式複雜得多,分四步實現:

(1)第一步,檢查自賦值。你可能會認為多此一舉,難道有人會愚蠢到寫出 a = a 這樣的自賦值語句!的確不會。但是間接的自賦值仍有可能出現,例如

 

// 內容自賦值

b = a;

c = b;

a = c; 

// 地址自賦值

b = &a;

a = *b;

 

也許有人會說:“即使出現自賦值,我也可以不理睬,大不了化點時間讓物件複製自己而已,反正不會出錯!”

他真的說錯了。看看第二步的delete,自殺後還能複製自己嗎?所以,如果發現自賦值,應該馬上終止函式。注意不要將檢查自賦值的if語句

if(this == &other)

錯寫成為

  if( *this == other)

(2)第二步,用delete釋放原有的記憶體資源。如果現在不釋放,以後就沒機會了,將造成記憶體洩露。

(3)第三步,分配新的記憶體資源,並複製字串。注意函式strlen返回的是有效字串長度,不包含結束符‘’。函式strcpy則連‘’一起復制。

(4)第四步,返回本物件的引用,目的是為了實現象 a = b = c 這樣的鏈式表達。注意不要將 return *this 錯寫成 return this 。那麼能否寫成return other 呢?效果不是一樣嗎?

不可以!因為我們不知道引數other的生命期。有可能other是個臨時物件,在賦值結束後它馬上消失,那麼return other返回的將是垃圾。

偷懶的辦法處理複製建構函式與賦值函式

  如果我們實在不想編寫複製建構函式和賦值函式,又不允許別人使用編譯器生成的預設函式,怎麼辦?

  偷懶的辦法是:只需將複製建構函式和賦值函式宣告為私有函式,不用編寫程式碼。

例如:

  class A

  { …

    private:

    A(const A &a);      // 私有的複製建構函式

  A & operate =(const A &a);  // 私有的賦值函式

  };

 

如果有人試圖編寫如下程式:

  A  b(a);  // 呼叫了私有的複製建構函式

  b = a;    // 呼叫了私有的賦值函式

編譯器將指出錯誤,因為外界不可以操作A的私有函式。

如何在派生類中實現類的基本函式

  基類的建構函式、解構函式、賦值函式都不能被派生類繼承。如果類之間存在繼承關係,在編寫上述基本函式時應注意以下事項:

u  派生類的建構函式應在其初始化表裡呼叫基類的建構函式。

u  基類與派生類的解構函式應該為虛(即加virtual關鍵字)。例如

#include

class Base

{

  public:

  virtual ~Base() { cout<< "~Base" << endl ; }

};

 

class Derived : public Base

{

  public:

  virtual ~Derived() { cout<< "~Derived" << endl ; }

};

 

void main(void)

{

  Base * pB = new Derived;  // upcast

  delete pB;

}

 

輸出結果為:

  ~Derived

  ~Base

如果解構函式不為虛,那麼輸出結果為

  ~Base

 

u  在編寫派生類的賦值函式時,注意不要忘記對基類的資料成員重新賦值。例如:

class Base

{

  public:

  Base & operate =(const Base &other);  // 類Base的賦值函式

  private:

  int  m_i, m_j, m_k;

};

 

class Derived : public Base

{

  public:

  Derived & operate =(const Derived &other);  // 類Derived的賦值函式

  private:

  int  m_x, m_y, m_z;

};

 

Derived & Derived::operate =(const Derived &other)

{

  //(1)檢查自賦值

  if(this == &other)

    return *this;

 

  //(2)對基類的資料成員重新賦值

  Base::operate =(other);  // 因為不能直接操作私有資料成員

 

  //(3)對派生類的資料成員賦值

  m_x = other.m_x;

  m_y = other.m_y;

  m_z = other.m_z;

 

  //(4)返回本物件的引用

  return *this;

}

 

一些心得體會

有些C++書籍稱建構函式、解構函式和賦值函式是類的“Big-Three”,它們的確是任何類最重要的函式,不容輕視。

也許你認為本章的內容已經夠多了,學會了就能平安無事,我不能作這個保證。如果你希望吃透“Big-Three”,請好好閱讀參考文獻[Cline] [Meyers] [Murry]。

 

 

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-991845/,如需轉載,請註明出處,否則將追究法律責任。

相關文章