C++中的建構函式設計

來源:互聯網
上載者:User
 在C++中,建構函式是一個在構件對象的時候調用的特殊的函數,其目的是對對象進行初始化的工作,從而使對象被使用之前可以處於一種合理的狀態。但是,建構函式的設計並不完美,甚至有些不合理的特性。比如說,限定建構函式名稱與類的名稱相同的條件。這些特性在構造C++編譯器的時候是值得引起注意的。還有,在今後C++的標準修訂或者制定其他物件導向的設計語言時候應當避免這些特性。這裡也提出了一些解決的方案。

    C++中,任何類都有一個(至少有一個)建構函式,甚至在沒有建構函式被聲明的時候亦是如此。在對象被聲明的時候,或者被動態產生的時候,這些建構函式就會被調用。建構函式做了許多不可見的工作,即使建構函式中沒有任何代碼,這些工作包括對對象的記憶體配置和通過賦值的方式對成員進行初始化。建構函式的名稱必須與類的名稱相同,但是可以有許多不同的重載版本來提供,通過參數類型來區分建構函式的版本。建構函式可以顯式的通過使用者代碼來調用,或者當代碼不存在是通過編譯器來隱式插入。當然,顯式地通過代碼調用是推薦的方法,因為隱式調用的效果可能不是我們所預料的,特別是在處理動態記憶體分配方面。代碼通過參數來調用唯一的建構函式。建構函式沒有傳回值,儘管在函數體中可以又返回語句。每個建構函式可以以不同的方式來執行個體化一個對象,因為每個類都有建構函式,至少也是預設建構函式,所以每個對象在使用之前都相應的使用建構函式。建構函式的調用1所示。

    圖1. The activities involved in the execution of a constructor

    因為建構函式是一種函數,所以他的可見度無非是三種public、private、protected。通常,建構函式都被聲明為public型。如果建構函式被聲明為private或protected,就限制了對象的執行個體化。這在阻止類被其他人執行個體化的方面很有效。建構函式中可以有任何C++的語句,比如,一條列印語句,可以被加入到建構函式中來表明調用的位置。

    建構函式的類型

    C++中建構函式有許多種類型,最常用的式預設建構函式和拷貝建構函式,也存在一些不常用的建構函式。下面介紹了四種不同的建構函式。

    1、預設建構函式

    預設建構函式是沒有參數的函數。另外,預設建構函式也可以在參數列表中以參數預設值的方式聲明。預設建構函式的作用是把對象初始化為預設的狀態。如果在類中沒有顯式定義建構函式,那麼編譯器會自動的隱式建立一個,這個隱式建立的建構函式和一個空的建構函式很相像。他除了產生對象的執行個體以外什麼工作都不做。在許多情況下,預設建構函式都會被自動的調用,例如在一個對象被聲明的時候,就會引起預設建構函式的調用。

    2、拷貝建構函式

    拷貝建構函式,經常被稱作X(X&),是一種特殊的建構函式,他由編譯器調用來完成一些基於同一類的其他對象的構件及初始化。它的唯一的一個參數(對象的引用)是不可變的(因為是const型的)。這個函數經常用在函數調用期間於使用者定義型別的值傳遞及返回。拷貝建構函式要調用基類的拷貝建構函式和成員函數。如果可以的話,它將用常量方式調用,另外,也可以用非常量方式調用。

    在C++中,下面三種對象需要拷貝的情況。因此,拷貝建構函式將會被調用。

    1). 一個對象以值傳遞的方式傳入函數體

    2). 一個對象以值傳遞的方式從函數返回

    3). 一個對象需要通過另外一個對象進行初始化

    以上的情況需要拷貝建構函式的調用。如果在前兩種情況不使用拷貝建構函式的時候,就會導致一個指標指向已經被刪除的記憶體空間。對於第三種情況來說,初始化和賦值的不同含義是建構函式調用的原因。事實上,拷貝建構函式是由普通建構函式和賦值操作賦共同實現的。描述拷貝建構函式和賦值運算子的異同的參考資料有很多。

    拷貝建構函式不可以改變它所引用的對象,其原因如下:當一個對象以傳遞值的方式傳一個函數的時候,拷貝建構函式自動的被調用來產生函數中的對象。如果一個對象是被傳入自己的拷貝建構函式,它的拷貝建構函式將會被調用來拷貝這個對象這樣複製才可以傳入它自己的拷貝建構函式,這會導致無限迴圈。

    除了當對象傳入函數的時候被隱式調用以外,拷貝建構函式在對象被函數返回的時候也同樣的被調用。換句話說,你從函數返回得到的只是對象的一份拷貝。但是同樣的,拷貝建構函式被正確的調用了,你不必擔心。

    如果在類中沒有顯式的聲明一個拷貝建構函式,那麼,編譯器會私下裡為你制定一個函數來進行對象之間的位拷貝(bitwise copy)。這個隱含的拷貝建構函式簡單的關聯了所有的類成員。許多作者都會提及這個預設的拷貝建構函式。注意到這個隱式的拷貝建構函式和顯式聲明的拷貝建構函式的不同在於對於成員的關聯方式。顯式聲明的拷貝建構函式關聯的只是被執行個體化的類成員的預設建構函式除非另外一個建構函式在類初始化或者在構造列表的時候被調用。

    拷貝建構函式是程式更加有效率,因為它不用再構造一個對象的時候改變建構函式的參數列表。設計拷貝建構函式是一個良好的風格,即使是編譯系統提供的協助你申請記憶體預設拷貝建構函式。事實上,預設拷貝建構函式可以應付許多情況。

    3、使用者定義的建構函式

    使用者定義的建構函式允許對象在被定義的時候同時被初始化。這種建構函式可以有任何類型的參數。一個使用者定義的和其它類型的建構函式在類mystring 中得以體現:

    class mystring

    {......

    public: mystring(); // Default constructor

    mystring (mystring &src)

    // Copy constructor

    mystring (char * scr);

    // Coercion constructor

    mystring ( char scr[ ], size_t len);

    // User-Defined constructor

    };

    4、強制建構函式

    C++中,可以聲明一個只有一個參數的建構函式來進行類型轉換。強制建構函式定一個從參數類型進行的一個類型轉換(隱式的或顯式的)。換句話說,編譯器可以用任何參數的執行個體來調用建構函式。這樣做的目的是建立一個臨時執行個體來替換一個參數類型的執行個體。注意標準新近加入C++的關鍵字explicit 是用來禁止隱式的類型轉換。然而,這一特性還沒能被所有的編譯器支援。下面是一個強制建構函式的例子:

    class A

    {

    public :

    A(int ){ }

    };

    void f(A) { }

    void g()

    {

    A My_Object= 17;

    A a2 = A(57);

    A a3(64);

    My_Object = 67;

    f(77);

    }

    像A My_Object= 17;這種聲明意味著A(int)建構函式被調用來從整型變數產生一個對象。這樣的建構函式就是強制建構函式。

    普遍特性

    下面是一些C++建構函式的不合理設計,當然,可能還有其他一些不合理之處。但是,大多數情況下,我們還是要和這些特性打交道,我們要逐一說明。

    1、建構函式可以為內聯,但不要這樣做

    一般來講,大多數成員函數都可以在前面加入"inline"關鍵字而成為內嵌函式,建構函式也不例外,但是別這麼做!一個被定義為內聯的建構函式如下:

    class x

    {..........

    public : x (int );

    :

    :

    };

    inline x::x(int )

    {...}

    在上面的代碼中,函數並不是作為一個單獨的實體而是被插入到程式碼中。這對於只有一兩條語句的函數來說會提到效率,因為這裡沒有調用函數的開銷。

    用內聯的建構函式的危險性可以在定義一個靜態內聯建構函式中體現。在這種情況下,靜態建構函式應當是只被調用一次。然而,如果標頭檔中含有靜態內聯建構函式,並被其他單元包括的話,函數就會產生多次拷貝。這樣,在程式啟動時就會調用所有的函數拷貝,而不是程式應當調用的一份拷貝。這其中的根本原因是靜態函數是在以函數偽裝下的真實對象。

    應該牢記的一件事是內聯是建議而不是強制,編譯器產生內聯代碼。這意味著內聯是與實現有關的編譯器的不同可能帶來很多差異。另一方面,內嵌函式中可能包括比代碼更多的東西。建構函式被聲明為內聯,所有包含對象的建構函式和基類的建構函式都需要被調用。這些調用是隱含在建構函式中的。這可能會建立很大的內嵌函式段,所以,不推薦使用內聯的建構函式。

    2、建構函式沒有任何傳回型別

    對一個建構函式指定一個傳回型別是一個錯誤,因為這樣會引入建構函式的地址。這意味著將無法處理出錯。這樣,一個建構函式是否成功的建立一個對象將不可以通過返回之來確定。事實上,儘管C++的建構函式不可以返回,也有一個方法來確定是否記憶體配置成功地進行。這種方法是內建在語言內部來處理緊急情況的機制。一個預定好的函數指標 new-handler,它可以被設定為使用者定製的對付new操作符失敗函式,這個函數可以進行任何的動作,包括設定錯誤標誌、重新申請記憶體、退出程式或者拋出異常。你可以安心的使用系統內建的new-handler。最好的使建構函式發出出錯訊號的方法,就是拋出異常。在建構函式中拋出異常將清除錯誤之前建立的任何對象及分配的記憶體。

    如果建構函式失敗而使用異常處理的話,那麼,在另一個函數中進行初始化可能是一個更好的主意。這樣,程式員就可以安全的構件對象並得到一個合理的指標。然後,初始化函數被調用。如果初始化失敗的話,對象直接被清除。

    3、建構函式不可以被聲明為static

    C++中,每一個類的對象都擁有類資料成員的一份拷貝。但是,靜態成員則沒有這樣而是所有的對象共用一個靜態成員。靜態函數是作用於類的操作,而不是作用在對象上。可以用類名和作用控制操作符來調用一個靜態函數。這其中的一個例外就是建構函式,因為它違反了物件導向的概念。

    關於這些的一個相似的現象是靜態對象,靜態對象的初始化是在程式的一開始階段就進行的(在main()函數之前)。下面的代碼解釋了這種情況。

    MyClass static_object(88, 91);

    void bar()

    {

    if (static_object.count( ) > 14) {

    ...

    }

    }

    在這個例子中,靜態變數在一開始的時候就被初始化。通常這些對象由兩部分構成。第一部分是資料區段,靜態變數被讀取到全域的資料區段中。第二部分是靜態初始化函數,在main()函數之前被調用。我們發現,一些編譯器沒有對初始化的可靠性進行檢查。所以你得到的是未經初始化的對象。解決的方案是,寫一個封裝函數,將所有的靜態對象的引用都置於這個函數的調用中,上面的例子應當這樣改寫。

    static MyClass* static_object = 0;

    MyClass*

    getStaticObject()

    {

    if (!static_object)

    static_object =

    new MyClass(87, 92);

    return static_object;

    }

    void bar()

    {

    if (getStaticObject()->count() > 15)

    {

    ...

    }

    }

    4、建構函式不能成為虛函數

    虛建構函式意味著程式員在運行之前可以在不知道對象的準確類型的情況下建立對象。虛建構函式在C++中是不可能實現的。最通常遇到這種情況的地方是在對象上實現I/O的時候。即使足夠的類的內部資訊在檔案中給出,也必須找到一種方法執行個體化相應的類。然而,有經驗的C++程式員會有其他的辦法來類比虛建構函式。

    類比虛函數需要在建立對象的時候指定調用的建構函式,標準的方法是調用虛的成員函數。很不幸,C++在文法上不支援虛建構函式。為了繞過這個限制,一些現成的方法可以在運行時刻確定構件的對象。這些等同於虛建構函式,但是這是C++中根本不存在的東西。

    第一個方法是用switch或者if-else選擇語句來手動實現選擇。在下面的例子中,選擇是基於標準庫的type_info構造,通過開啟運行時刻類型資訊支援。但是你也可以通過虛函數來實現RTTI

    class Base

    {

    public:

    virtual const char* get_type_id() const;

    staticBase* make_object

    (const char* type_name);

    };

    const char* Base::get_type_id() const

    {

    return typeid(*this).raw_name();

    }

    class Child1: public Base

    {

    };

    class Child2: public Base

    {

    };

    Base* Base::make_object(const char* type_name)

    {

    if (strcmp(type_name,

    typeid(Child1).raw_name()) == 0)

    return new Child1;

    else if (strcmp(type_name,typeid

    (Child2).raw_name()) == 0)

    return new Child2;

    else

    {

    throw exception

    ("unrecognized type name passed");

    return 0X00; // represent NULL

    }

    }

    這一實現是非常直接的,它需要程式員在main_object中儲存一個所有類的表。這就破壞了基類的封裝性,因為基類必須知道自己的子類。

    一個更物件導向的方法類解決虛建構函式叫做標本執行個體。它的基本思想是程式中產生一些全域的執行個體。這些執行個體只再虛建構函式的機制中存在:

    class Base

    {

    public:

    staticBase* make_object(const char* typename)

    {

    if (!exemplars.empty())

    {

    Base* end = *(exemplars.end());

    list::iterator iter =

    exemplars.begin();

    while (*iter != end)

    {

    Base* e = *iter++;

    if (strcmp(typename,

    e->get_typename()) == 0)

    returne->clone();

    }

    }

    return 0X00 // Represent NULL;

    }

    virtual ~Base() { };

    virtual const char* get_typename() const

    {

    return typeid(*this).raw_name();

    }

    virtual Base* clone() const = 0;

    protected:

    static list exemplars;

    };

    list Base::exemplars;

    // T must be a concrete class

    // derived from Base, above

    template

    class exemplar: public T

    {

    public:

    exemplar()

    {

    exemplars.push_back(this);

    }

    ~exemplar()

    {

    exemplars.remove(this);

    }

    };

    class Child: public Base

    {

    public:

    ~Child()

    {

    }

    Base* clone() const

    {

    return new Child;

    }

    };

    exemplar Child_exemplar;

    在這種設計中,程式員要建立一個類的時候要做的是建立一個相應的exampler類。注意到在這個例子中,標本是自己的標本類的執行個體。這提供了一種高校得執行個體化方法。

    5、建立一個預設建構函式

    當繼承被使用的時候,卻省建構函式就會被調用。更明確地說,當繼承層次的最晚層的類被構造的時候,所有基類的建構函式都在派生基類之前被調用,舉個例子來說,看下面的代碼:

    #include

    class Base

    {

    int x;

    public :

    Base() : x(0) { } // The NULL constructor

    Base(int a) : x(a) { }

    };

    class alpha : virtual public Base

    {

    int y;

    public :

    alpha(int a) : Base(a), y(2) { }

    };

    class beta : virtual public Base

    {

    int z;

    public :

    beta(int a) : Base(a), z(3) { }

    };

    class gamma : public alpha, public beta

    {

    int w;

    public :

    gamma ( int a, int b) : alpha(a), beta(b), w(4) { }

    };

    main()

    {.....

    }

    在這個例子中,我們沒有在gamma的標頭檔中提供任何的初始化函數。編譯器會為基類使用預設的建構函式。但是因為你提供了一個建構函式,編譯器就不會提供任何預設建構函式。正如你看到的這段包含預設建構函式的代碼一樣,如果刪除其中的預設建構函式,編譯就無法通過。

    如果基類的建構函式中引入一些副效應的話,比如說開啟檔案或者申請記憶體,這樣程式員就得確保中間基類沒有初始化虛基類。也就是,只有虛基類的建構函式可以被調用。

    虛基類的卻省建構函式完成一些不需要任何依賴於衍生類別的參數的初始化。你加入一個init()函數,然後再從虛基類的其他函數中調用它,或在其他類中的建構函式裡調用(你的確保它只調用了一次)。

    6、不能取得建構函式的地址

    C++中,不能把建構函式當作函數指標來進行傳遞,指向建構函式的的指標也不可以直接傳遞。允許這些就可以通過調用指標來建立對象。一種達到這種目的的方法是藉助於一個建立並返回新對象的靜態函數。指向這樣的函數的指標用於新對象需要的地方。下面是一個例子:

    class A

    {

    public:

    A( ); // cannot take the address of this

    // constructor directly

    static A* createA();

    // This function creates a new A object

    // on the heap and returns a pointer to it.

    // A pointer to this function can be passed

    // in lieu of a pointer to the constructor.

    };

    這一方法設計簡單,只需要將抽象類別置入標頭檔即可。這給new留下了一個問題,因為準確的類型必須是可見的。上面的靜態函數可以用來封裝隱藏子類。

    7、位拷貝在動態申請記憶體的類中不可行

    C++中,如果沒有提供一個拷貝建構函式,編譯器會自動產生一個。產生的這個拷貝建構函式對對象的執行個體進行位拷貝。這對沒有指標成員的類來說沒什麼,但是,對用了動態申請的類就不是這樣的了。為了澄清這一點,設想一個對象以值傳遞的方式傳入一個函數,或者從函數中返回,對象是以為拷貝的方式複製。這種位拷貝對含有指向其他對象指標的類是沒有作用的(見圖2)。當一個含有指標的類以值傳遞的方式傳入函數的時候,對象被複製,包括指標的地址,還有,新的對象的範圍是這個函數。在函數結束的時候,很不幸,解構函式要破壞這個對象。因此,對象的指標被刪除了。這導致原來的對象的指標指向一塊空的記憶體地區-一個錯誤。在函數返回的時候,也有類似的情況發生。

    圖2. The automatic copy constructor that makes a bitwise copy of the class.

    這個問題可以簡單的通過在類中定義一個含有記憶體申請的拷貝建構函式來解決,這種靠叫做深拷貝,是在堆中分配記憶體給各個對象的。

    8、編譯器可以隱式指定強制建構函式

    因為編譯器可以隱含選取強制建構函式,你就失去了調用函數的選擇權。如果需要控制的話,不要聲明只有一個參數的建構函式,取而代之,定義helper函數來負責轉換,如下面的例子:

    #include

    #include

    class Money

    {

    public:

    Money();

    // Define conversion functions that can only be

    // called explicitly.

    static Money Convert( char * ch )

    { return Money( ch ); }

    static Money Convert( double d )

    { return Money( d ); }

    void Print() { printf( "\n%f", _amount ); }

    private:

    Money( char *ch ) { _amount = atof( ch ); }

    Money( double d ) { _amount = d; }

    double _amount;

    };

    void main()

    {

    // Perform a conversion from type char *

    // to type Money.

    Money Account = Money::Convert( "57.29" );

    Account.Print();

    // Perform a conversion from type double to type

    // Money.

    Account = Money::Convert( 33.29 );

    Account.Print();

    }

    在上面的代碼中,強制建構函式定義為private而不可以被用來做類型轉換。然而,它可以被顯式的調用。因為轉換函式是靜態,他們可以不用引用任何一個對象來完成調用。

    總結

    要澄清一點是,這裡提到的都是我們所熟知的ANSI C++能夠接受的。許多編譯器都對ANSI C++進行了自己的文法修訂。這些可能根據編譯器的不同而不同。很明顯,許多編譯器不能很好的處理這幾點。探索這幾點的緣故是引起編譯構造的注意,也是在C++標準化的過程中移除一些瑕疵。

    參考文獻:

    1. Stroustrup, Bjarne. The C++ Programming Language, 3rd ed., Addison-Wesley, Reading, MA, 1997. 2. Ellis, Margaret and Bjarne Stroustrup. The Annotated C++ Reference Manual, Addison-Wesley, Reading,   MA, 1990. 3. Stroustrup, Bjarne. The Design and Evolution of C++, Addison-Wesley, Reading, MA, 1994. 4. Murry, Robert B. C++ Strategies and Tactics, Addison-Wesley, Reading, MA, 1993. 5. Farres-Casals, J. "Proving Correctness of Constructor Implementations," Mathematical Foundations of   Computer Science 1989 Proceedings. 6. Breymann, Ulrich. Designing Components with the C++ STL, Addison-Wesley, Reading, MA,1998. 7. Lippman, Stanley and Josee LaJoie. C++ Primer, 3rd ed., Addison-Wesley, Reading, MA, 1998. 8. Skelly, C. "Getting A Handle On The New-Handler," C++ Report, 4(2):1-18, February 1992. 9. Coggins, J. M. "Handling Failed Constructors Gracefully," C++ Report, 4(1):20-22, January 1992. 10. Sabatella, M. "Laser Evaluation of C++ Static Constructors," SIGPLAN Notices, 27(6):29-36 (June     1992). 11. Eckel, B. "Virtual Constructors," C++ Report, 4(4):13-16,May 1992. 12. Coplien, James O. Advanced C++: Programming Styles and Idioms, Addison-Wesley, Reading, MA, 1992.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.