C++初學解惑(4)——建構函式(上) (轉)

amyz發表於2007-08-16
C++初學解惑(4)——建構函式(上) (轉)[@more@]

構造():namespace prefix = o ns = "urn:schemas--com::office" />

  為了便於說明建構函式存在的意義和用法,不妨假設我們正著手於某個或者某些class的設計。這裡我們假想要設計這個兩個class,一個是描述複數的class,即Complex.,雖然標準庫中也存在複數類,但假設出於特殊需要我們要自己設計一個,它看起來或許是類似這樣子:

class Complex

{

public:

  ... // others

private:

  double _x;  // 實部

  double _y;  // 虛部

};

  此外我們要還設計一個class,它是一個用於int型資料的高階陣列,我們知道C++中的陣列功能單一,存在無法動態擴充、不夠等問題,所以我們要進行更高階的設計,它大概相當於標準庫中的vector.由於涉及到的動態分配,我們需要一個指標變數來儲存分配所得的記憶體塊地址,或許我們同時還需要一個表示當前陣列元素個數的整型變數,所以它看起來是這樣子:

class IntArray

{

public:

  ... // others

private:

  int *_p;

  int _size;

};

  OK,我們現在開始看看建構函式可以為我們這兩個class提供什麼樣的好處。

一、初始化,以及初始化的多種途徑

  引入建構函式一個很自然的目的就是用於在構建時對其進行初始化。沒有被初始化的物件,其值將是任意的,如果對其進行不恰當的使用,可能會引起一定的危險。例如對於IntArray,假如不提供初始化,則物件在構造之後,_p和_size的值一般是不確定的,假如此時不小心對其進行陣列讀寫操作,則幾乎可以肯定它將訪問到某個不該訪問的區域,結局是出現記憶體保護錯誤,或者崩潰。對於複數類,情況則好一點:一個初始值不確定的複數至少不會那麼直接地引起上述恐怖的錯誤;但我們仍有理由希望它們都有一個自動的初始化機制。當然,建構函式可以為我們實現這一點。

  對於IntArray,可以令其在初始化時將_p設為空指標,將_size設為0(以下給出定義,略去宣告)::

IntArray::IntArray()

{

  _p = 0;

  _size = 0;

}

  對於複數類,則簡單地將初始後的實、虛部設為0:

Complex::Complex()

{

  _x = 0.0;

  _y = 0.0;

}

  現在我們的兩個class都提供了方便安全的初始化功能。但再進一步,我們有時希望能夠用一個具體的、指定的方式來初始化物件,例如,我們可能想構造出一個值為3+2i的複數變數,如果建構函式只提供簡單的構造功能,則我們不得不寫:

  Complex c;

  c.setX(3.0);  // 設定實部

  c.setY(2.0);  // 設定虛部

  呃,當然,這看起來沒有太大的問題,至少它可以正常地工作。只是我們注意到在程式碼的第一行,定義了複數物件c,而這將建構函式,它分別將_x,_y設為0,然後我們又要再手動地將_x,_y分別改為3和2,多少有些愚蠢,不是麼?即使不考慮的損失,看起來也頗不雅觀。所幸我們可以使用帶引數的建構函式來實現這一點:

Complex::Complex(double x, double y)

{

  _x = x;

  _y = y;

}

  由於我們需要同時提供預設初始化方式和指定初始化方式,所以應當使用過載,保留原來的建構函式。我們可以像前面宣告c那樣使用預設的初始化方式,也可以使用指定初值的方式,像這樣:

  Complex d(3.0, 2.0);

  此時將呼叫第二個版本的建構函式,構建出一個值為3+2i的複數物件d.

  上面我們以兩個過載的建構函式實現了兩種不同的初始化方式,觀察發現它們其實是很相像的,即都是為_x,_y進行賦值,區別僅在於所賦的值是預設給定還是特別指定。對於這一特點我們不難聯想到這就是預設引數函式的特徵,從而我們可以將上述兩個建構函式合併成為一個具有預設引數的建構函式:

Complex::Complex(double x = 0.0, double y = 0.0)

{

  _x = x;

  _y = y;

}

  除了觀感更好之外,我們還“順便”獲得了一個好處:可以用一個實數來初始化複數:

  Complex c(3.14);

  由於虛部沒有明確給出,所以它將被預設為0,這是符合實際意圖的;從而它免去了我們再多寫一個專門用一個實數引數初始化複數的建構函式。

  當然,不是所有的情況下,類似這樣“順便”的功能都是好處:它當然也可能帶來混亂或者不合實際的初始化,這時我們就有必要透過過載來具體限定初始化的途徑。

  類似地,對於IntArray,透過過載我們可以實現各種初始化方式,例如,透過指定_size來讓其實現初始時就分配記憶體,更進一步,還可以再指定一個初始值,賦予陣列中的每一個元素作為初始值;我們也可以用一個普通的C++整型陣列(或者它的指定的某一部分)來初始化IntArray,等等。

二、具有特別建構函式的class

1.不存在預設構造方式的class

  對於前述各版本的Complex類,我們都提供了它的“預設構造”的途徑,即我們可以寫形如:

  Complex c;

的語句而不必須在物件名後面加上一對含有構造引數的括號。因為當我們沒有對Complex提供任何建構函式,或者僅提供一個無實參的、形如Complex()的建構函式時,這也是我們宣告Complex物件的唯一方式,到我們使用過載、乃至將它們統一成一個提供預設引數的建構函式時,這種寫法仍然是有意義的。但假如我們僅僅提供一個建構函式,並且它需要指定引數,同時還不允許完全的預設引數,如

  Complex(double x, double y);

  這將意味著像Complex()這樣的函式呼叫是沒有意義的,也就是說,我們不能再寫

  Complex c;  // 不存在Complex()這樣的函式供呼叫,編譯出錯

  而只能寫類似於這樣的語句。

  Complex c(0.0,0.0);  // 沒問題,它呼叫Complex(double x, double y)

  哦,看起來問題不算太大,我們只不過在宣告物件的時候多寫點東西就可以了。但讓我們把注意力轉移到C++的陣列:假如我們需要宣告一個Complex陣列,應該怎麼辦呢?顯然Complex仍然會要求我們對每個物件提供初始值,但對於陣列,我們無法寫

  Complex a[20](0.0, 0.0);  // 哈,出錯了

  同樣,動態分配陣列也不起作用:

  Complex *p = new Complex[20](0.0, 0.0);  // 哈哈,還是不行

  我們只能承認,“不存在直接的方法,可以讓沒有提供預設構造方式的類物件使用陣列。”呵,你可能要提出這個說法似乎在暗示還是存在某些隱澀的方法來實現Complex陣列。那讓我想一想,呃,或許你可以用C中的malloc()記憶體分配函式來實現它,由於它只是單純地進行記憶體分配而不會呼叫建構函式,所以編譯不會有問題。但用malloc()來為C++物件分配記憶體是很過時的方法,因而也不被推薦使用。如果你真的覺得迫切需要使用陣列,那還是為你的類提供一個預設構造的介面吧。

  此外,如果有一個類需要包含Complex物件作為它的成員,應該如何對其指定初始化引數呢?例如:

class A

{

public:

  ... // others

private:

  ... // others

  Complex c;  // 這裡不允許寫Complex c(0.0,0.0);

};

  這裡,C++專門為之提供了初始化方式,即“成員初始化列表”。它出現在建構函式定義的參數列後,以冒號與參數列分隔,如果有多個成員需要在參數列內初始化,則它們之間以逗號分隔。對於class A,為了對成員c進行初始化,我們可以這樣定義建構函式:

A::A(... /* 參數列 */) : c(0.0, 0.0) // 在這裡初始化c

{

  ... // 其它操作

}

  當然,上面的(0.0,0.0)還可以改成包含其它引數、變數等等的參量,以實現對c值的指定。

  除了定義的class物件外,C++中的內建基本型別也可以在成員初始化列表中完成初始化,例如前面的Complex建構函式也可以寫成

Complex::Complex(double x = 0.0, double y = 0.0) : _x(x), _y(y)

{

}

  那麼,它和

Complex::Complex(double x = 0.0, double y = 0.0)

{

  _x = x;

  _y = y;

}

  有什麼區別呢?答案是沒有區別。有些人,比如我,更喜歡把基本內建型別的初始化也弄進初始化列表中;而另一些人則傾向於只把class物件放進去,而把基本內轉型別的初始化放在函式體中。你可以擇好而從之。

  如果建構函式既包含了成員初始化列表,也包含了函式體內的語句(包括初始化語句與非初始化語句),那麼則保證先將初始化列表的初始化完成,再函式體的語句。但對於成員初始化列表中各成員的初始化次序,則需要特別注意:該次序並不是由書寫次序決定,而是由class宣告中各成員的宣告次序決定。如考慮前面的IntArray,假設我們要提供一個“以陣列大小”初始化的建構函式,則它可能是這樣子:

class IntArray

{

public:

  IntArray(int size);

  ... // others

private:

  int *_p;

  int _size;

};

// 簡單起見,假設使用者提供的size值總大於0,以保證new int[size]總有意義

IntArray::IntArray(int size): _size(size), _p(new int[_size])

{

  ... // others

}

  這裡就引入了一個相當隱匿的錯誤:表面上看,我們在初始化列表中先初始化了_size,再以初始化後的_size作為動態陣列的大小傳給new。但初始化列表中物件的初始順序實際上是依據變數的宣告順序,而我們在IntArray的宣告部分中先宣告_p,爾後才宣告_size,因而程式執行時將先會對_p進行初始化,而此時new表示式所用到的_size值是不確定的,可能是一個非常大的數,也可能非常小甚至是負值,從而嚴重違背了我們的本意。

  避免出現上述錯誤當然可以有很多種方法,比如,直接一點地,可以_p與_size的宣告次序,但這不是一個值得提倡的方法,畢竟我們潛意識中都會認為類的宣告次序不會改變程式的語義,因而有可能在將來什麼時候更動宣告次序,而這將導致錯誤,並且難以捕捉。一個比較好的解決方案是將_size改為size,也就是在初始化階段應當儘量降低成員變數之間的依賴性。

  以上我們討論了一個沒有包含預設構造形式的類Complex,以及當另一個類將Complex作為成員變數時應當如何對其進行初始化,下面我們看看繼承的情況:假如有一個class繼承自Complex,根據關於繼承的規定,編譯器將在派生類的建構函式呼叫完成之後再呼叫基類,也就是Complex的建構函式。問題是我們應當如何將引數傳給基類的建構函式?答案是類似的:透過成員初始化列表。下面是例子:

class SuperComplex: public Complex

{

public:

  SuperComplex(... /* 參數列 */);

  ... // others

private:

  ... // others

};

SuperComplex::SuperComlex(... /* 參數列 */): Complex(0.0, 0.0)

{

  ... // others

}

  顯然繼承的情況與前面包含的情況是很相似的,不同之處是將前面的物件名換成了這裡的基類名,當然你也可以用其它變數值,比如SuperComplex的建構函式的引數值來代替Complex(0.0, 0.0)中的常數引數。

  2. 私有建構函式

  通常我們都將建構函式的宣告置於public區段,假如我們將其放入private區段中會發生什麼樣的後果?沒錯,我也知道這將會使建構函式成為私有的,這意味著什麼?

  我們知道,當我們在程式中宣告一個物件時,編譯器為呼叫建構函式(如果有的話),而這個呼叫將通常是外部的,也就是說它不屬於class物件本身的呼叫,假如建構函式是私有的,由於在class外部不允許訪問私有成員,所以這將導致編譯出錯。

  你於是說:“哈哈。”我們製造了一個似乎無法產生物件的class.哦,當然,對於class本身,我們還可以利用它的static公有成員,因為它們獨立於class物件之外,我們不必產生物件也可以使用它們。嗯,看來我們還是為帶有私有建構函式的類找到了一個存在的理由。不過我們不應當滿足於此,因為看上去應當還有發掘的餘地。

  首先我們來認真看一下是不是真的無法建立出一個具有私有建構函式的類物件。“呃,可能未必。”你現在也許會這樣說。這很好,讓我們再來看看為什麼,沒錯,因為建構函式被class私有化了,所以我們要建立出物件,就必須能夠訪問到class的私有域;但這一點“我們”是做不到的,那麼,誰能做得到呢?class的成員可以做得到;但在我們建構出其物件之前,怎麼能利用它的成員呢?噢,剛才我們剛剛提到了static公有成員,它是獨立於class物件而存在的,當然,它也是公有的,“我們”可以訪問得到。假如在某個static函式中建立了該class的物件,並以引用或者指標的形式將其返回(不可以以值的形式返回,想想為什麼),我們就獲得了這個物件的使用權。下面是例子:

class WonderfulClass

{

public:

  static WonderfulClass* makeAn()

  {

  // 建立一個WonderfulClass物件並返回其指標

  return (new WonderfulClass);

  }

private:

  WonderfulClass() { }

};

int main()

{

  WonderfulClass *p = WonderfulClass::makeAnObject();

  ... // 使用*p

  delete p;  // Not neccesary here, but it's a good habit.

  return 0;

}

  嗯,這個例子使用了私有建構函式,但它執行得很好:makeAnObject()作為WonderfulClass的靜態成員函式,盡心盡責地為我們建立物件:由於要跨函式傳遞並且不能使用值傳遞方式,所以我們選擇在堆上建立物件,這樣即使makeAnObject()退出,物件也不會隨之蒸發掉,當然,使用完之後你可不要忘了手工將它清除。

  回到前面的思路:除了公有的static成員可以幫助我們訪問私有域外,還有沒有其它可以利用的“東西”?

  噢,你一定想到了使用友元,完全正確。可以使用該類的友元函式或者友元類建立其物件,這裡就不舉例了。

  我們知道沒有人會無聊到無緣無故把一個class設為私有,然後再寫一個和上面一模一樣的makeAnObject()來讓它的使用者體驗一下奇特的感覺。我們也不太相信這只是由於C++的設計原因而導致的一個“順便的”“特殊的”“無用的”邊角功能。它應當是有實際用途的。

  嗯,例如,我們想實現這樣一個class:它至多隻能存在一個,或者指定數量個的物件(還記得標準輸入輸出流庫中那個獨一無二的cout嗎?),我們可以在class的私有域中新增一個static型別的計數器,它的初值置為0,然後再對makeAnObject()做點手腳:每次呼叫它時先檢查計數器的值是否已經達到物件個數的上限值,如果是則產生錯誤,否則才new出新的物件,同時將計數器的值增1.最後,為了避免值複製時產生新的物件副本,除了將建構函式置為私有外,複製建構函式也要特別宣告並置為私有。


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

相關文章