天天看點

淺析拷貝構造函數

這篇文章,主要是受jinhao (辣子雞丁·game就這樣over了 )在csdn上一篇題為《有關拷貝構造函數的說法不正确的是》的文章啟發,雞丁就這四個問題回答如下。

   拷貝構造函數的名字和類名是一樣的 [錯]

   類中隻有一個拷貝構造函數 [錯]

   拷貝構造函數可以有多個參數 [對]

         拷貝構造函數無任何函數類型 [錯]

在這裡我不想讨論以上問題的正确與錯誤,隻是讨論一下構造函數,拷貝構造函數,可能還會涉及到指派函數,析構函數,這些都屬于類中的特殊函數。至于以上問題的正誤,由讀者您來決定。

讨論這些問題前,我們來基本了解一個預設構造函數,拷貝構造函數:

1:關于預設構造函數,拷貝構造函數,析構函數,指派函數,c++标準中提到

the default constructor , copy constructor and copy assignment operator , and destructor are special member functions. the implementation will implicitly declare these member functions for a class type when the program does not explicitly declare them, except as noted in 12.1. the implementation will implicitly define them if they are used。這段話的意思是說,以上四個函數如果在使用的過程中,發現程式沒有顯示聲明,将隐式執行生成這些聲明。是以這裡有可能給我們帶來一個麻煩就是如果我們聲明的預設構造函數,拷貝構造函數,析構函數,指派函數不正确(非文法層次),那麼我們的class一樣可以執行以上提到的操作。

2:這些特殊函數遵循一般函數通路規則,比如在類中聲明一個保護類型構造函數,隻有它的繼承類和友元類才可以使用它建立對象。special member functions obey the usual access rules (clause 11). [example: declaring a constructor protected ensures that only derived classes and friends can create objects using it.],但是也有其特殊性,不然也不會叫特殊函數,稍後我們逐漸讨論他們的特殊性。

3:關于構造函數的名稱,構造函數的名字是否一定要與類名相同?按c++标準說,構造函數是沒有名字的。也許你看到這裡後會驚訝,但是确實是這樣。constructors do not have names. a special declarator syntax using an optional sequence of functions pecifiers(7.1.2) followed by the constructor’s class name followed by a parameter list is used to declare ordefine the constructor. in such a declaration, optional parentheses around the constructor class name are ignored.。構造函數屬于特殊處理函數,它沒有函數名稱,通過函數名也無法找到它。确認調用構造函數的是通過參數鍊中參數順序。

因為構造函數的特殊性,它不同于普通函數的一點是,const,static,vitual, volatile對構造函數的修飾無效

class caboutconstructer

{

public:

    static caboutconstructer()

    {

       _itemp = 1;

       cout << "constuctor" << endl;

    }

private:

    int _itemp;

};

如上,如果把構造函數聲明為static類型編譯器将傳回錯誤:

error c2574: “caboutconstructer::caboutconstructer” 構造函數和析構函數不能聲明為靜态的

同樣如果聲明為const,編譯器将傳回錯誤:c2583 “caboutconstructer::caboutconstructer” : “const”“this”指針對于構造函數/析構函數是非法的。

但是構造函數可以是inline,隐式聲明的構造函數也是inline類型的。這條規則也适用析構函數,唯一不同就是析構函數可以是virtual,而構造函數不能是virtual的。這裡可能涉及到的一個問題是,把析構函數聲明為inline virtual或者virtual inline會怎麼樣?其實這時的析構函數又顯示出其一般函數的特性,這時的析構函數和普通函數在對待virtual inline問題是一樣的,inline屬于編譯時刻展開,而virtual是運作時刻綁定。我們的編譯器不能做到使我們的程式既有inline帶來的速度,又有virtual帶來的運作時刻差別。

4:什麼情況下需要顯示聲明構造函數,并不是任何情況都需要顯示聲明構造函數的,比方說,聲明的類不存在虛函數,不存在繼承關系或者所有的非靜态資料都沒有顯示聲明構造函數,或者所有的非靜态資料不需要初始化為特定值,那麼這種情況下也沒有必要顯示聲明構造函數。這條規則同樣适合拷貝構造函數。

5:拷貝構造函數也是一個構造函數,其第一個參數值必須為type x&,或者type const x&類型,并且沒有其他參數或者其他參數都有預設值,我們看c++标準中的一個例子

[example: x::x(const x&) and x::x(x&, int=1)

are copy constructors.

class x {

// ...

x(int);

x(const x&, int = 1);

x a(1); // calls x(int);

x b(a, 0); // calls x(const x&, int);

x c = b; // calls x(const x&, int);

—end example] [note: all forms of copy constructor may be declared for a class. [example:

x(const x&);

x(x&); //ok

現在總結一下:構造函數是一種特殊函數,而拷貝構造函數是一種特殊的構造函數,拷貝構造函數的第一個參數必須為type x&,或者type const x&,要麼不存在其他參數,如果存在其他參數,其他參數必須有預設值,不妨加一句,根據這些定義可以确定一個類中可以有多個拷貝構造函數,但是我們根據拷貝構造函數的應用,即在指派對象操作,對象作為參數時傳遞,以及對象作為傳回值傳回和在異常中抛出對象時,都需要調用類的拷貝構造函數生成對象這一點來定義拷貝構造函數,那麼類中是否還可以定義多個拷貝構造函數,即理論上可以,實際中是否也可以定義多個拷貝構造函數?這個問題我們先保留,稍後讨論,

構造函數,拷貝構造函數都沒有傳回值,如果程式沒有顯示聲明或者顯示聲明錯誤(ill -formed),都會生成相應的預設構造函數,拷貝構造函數,析構函數等。這些工作都有我們的編譯器在編譯的時候幫我們做好。

看過這些标準後,我的感覺就是c++難學,而c++編譯器更難做,因為編譯器要幫我們做太多的事情。這些都是題外話,我們将繼續我們的構造函數之旅。

也就是因為構造函數的特殊性(沒有函數名,不具有傳回值,編譯器可以預設建立,一般函數可享受不了這種待遇,這還不特殊嗎),作為特殊函數,那麼必須尤其特殊性,才能彰顯出與衆不同。

1:explicit關鍵字就是為構造函數準備的。這個關鍵字的含義就是顯示調用構造函數,禁止編譯器轉換。确實編譯器幫我們做太多的轉換了,有時編譯器的這種好意會給我們帶來麻煩,是以我們要控制編譯器。

    explicit caboutconstructer(int ivalue)

       _itemp = ivalue;

    inline void p_show() const

       cout << _itemp << endl;

caboutconstructer a(1);

a.p_show();

a = 6; // 如果caboutconstructer聲明為explicit,那麼此處無法編譯,我們需要,顯示調用caboutconstructer,聲明如下a = caboutconstructer(6);

2:對象初始化清單,參考下面構造函數中對_itemp,不同的初始化方式。

    caboutconstructer(int ivalue):_itemp(ivalue)

//     _itemp = ivalue;

對象的構造過程是,類的構造函數首先調用類内變量的構造函數(在c++中我們應該也把int等内置類型看作一個對象,是一個類,比方說我們可以這樣定義一個int類型變量int i(5);這裡的功能相對于int i; i = 5;,不過這兩種方式是等效的,可以檢視反彙編代碼,這麼不做過多解釋)。那麼調用構造函數有兩種方式1:調用預設構造函數2:按值構造對象,在這裡就是應用2特性,即構造函數在初始化_itemp時直接把ivalue傳遞給_itemp,這樣減少了後面指派操作_itemp = ivalue;,是以初始化清單的效率相對于普通的指派操作要高。

    構造函數中拷貝構造函數,拷貝構造函數的定義非常簡單:拷貝構造函數是一個構造函數,其第一個參數必須為type x&或者type const x&類型,并且沒有其他參數,或者其他參數都有預設值。那麼如下聲明方式都應該是正确

     caboutconstructer(caboutconstructer &rvalue);

    caboutconstructer(const caboutconstructer &rvalue);

    caboutconstructer(caboutconstructer& rvalue,int ipara = 0);

    caboutconstructer(const caboutconstructer& rvalue,int ipara = 0);

    caboutconstructer(caboutconstructer& rvalue,int ipara1 = 0,int ipara2 = 0);

    caboutconstructer(const caboutconstructer& rvalue,int ipara = 0,int ipara2 = 0);

測試一下,除了在編譯時刻有一個warnging外,編譯成功。

warning c4521: “caboutconstructer” : 指定了多個複制構造函數

這個warning提示我們說聲明了多個複制(拷貝)構造函數,這個warning的含義寫的有點不明白

編譯器警告(等級 3)c4521

“class”: 指定了多個複制構造函數

類有單個類型的多個複制構造函數。使用第一個構造函數。

我們不管它,現在至少說明一點拷貝構造函數可以在形式上定義多個,但是形式上的定義,能否經的住考驗。看下面這個例子,我們先從函數重載說起

       void p_show(int i) const;

       void p_show(int i,int j = 0) const;

       void p_show(caboutconstructer &rvalue) const;

       void p_show(const caboutconstructer &rvalue) const;

       void p_show(caboutconstructer& rvalue,int ipara = 0) const;

       void p_show(const caboutconstructer& rvalue,int ipara = 0) const;

上面這幾種聲明方式,在我們編譯時,居然沒有報二義性錯誤,編譯通過了,很奇怪。但是當我們使用上面的函數時

int _tmain(int argc, _tchar* argv[])

    caboutconstructer a(1);

    a.p_show(1);

    system("pause");

    return 0;

}

再次編譯,發生一個錯誤

error c2668: “caboutconstructer::p_show” : 對重載函數的調用不明确

可能是“void caboutconstructer::p_show(int,int) const”

或是“void caboutconstructer::p_show(int) const”

編譯器錯誤 c2668

“function”: 對重載函數的調用不明确

未能解析指定的重載函數調用。可能需要顯式轉換一個或多個實際參數。

那麼從上面我們可以得出一點,編譯器隻有在使用函數時,才會對函數進行二義性檢查,或者說實作時。

看到這裡不得不想,拷貝構造函數會不會也這樣那?聲明時沒有問題,而在實際應用過程中出錯。

那麼我們做如下測試

// 第一種形式

    caboutconstructer a1(a);

    // 第二種形式

    const caboutconstructer b(1);

    caboutconstructer b1(b);

    // 第三種形式

    b1 = a1;

    // 第四種形式

    b1.p_show(a1);

    // 第五種形式

    a1.p_show(b);

為了測試函數傳遞對象,我們定義如下兩個函數,其目的就是在一個對象内顯示另一個對象的_itemp值,把上面的先注釋掉,編譯如下兩個函數,

void p_show(caboutconstructer rvalue) const

       rvalue.p_show();

    void p_show(const caboutconstructer rvalue) const

編譯出錯

error c2535: “void caboutconstructer::p_show(caboutconstructer) const” : 已經定義或聲明成員函數。

這說明在按值傳遞的函數重載時,不能通過對一個參數添加const來實作函數重載,但是修改把上面參數傳遞修改為引用方式,就可以通過添加const來實作函數重載,這些都是題外話,畢竟我們在這裡要測試的是,參數按值傳遞時,調用拷貝構造函數的問題,不知道這條規則是否也适合拷貝構造函數?拷貝構造函數是按引用方式傳遞。這一點是和普通函數調用方式一樣。

編譯正确。

為了驗證拷貝構造函數,現在我們也把拷貝構造函數實作,如下

caboutconstructer(caboutconstructer &rvalue)

       _itemp = rvalue._itemp;

    caboutconstructer(const caboutconstructer &rvalue)

    caboutconstructer(caboutconstructer& rvalue,int ipara = 0)

    caboutconstructer(const caboutconstructer& rvalue,int ipara = 0)

    caboutconstructer(caboutconstructer& rvalue,int ipara1 = 0,int ipara2 = 0)

    caboutconstructer(const caboutconstructer& rvalue,int ipara = 0,int ipara2 = 0)

編譯通過,期待中的二義性還是沒有出現。

好,現在開始測試第一種情況

caboutconstructer a1(a);

編譯,期待中的二義性終于出現了

error c2668: “caboutconstructer::caboutconstructer” : 對重載函數的調用不明确可能是“caboutconstructer::caboutconstructer(caboutconstructer &,int,int)” 或“caboutconstructer::caboutconstructer(caboutconstructer &,int)”或“caboutconstructer::caboutconstructer(caboutconstructer &)”

好那麼先注釋掉一些,僅保留

測試沒有問題,繼續擴大拷貝構造函數範圍,加上

caboutconstructer(const caboutconstructer &rvalue)

編譯通過,居然沒有問題,也沒有二義性。但是此時

究竟調用的那個拷貝構造函數那?我們跟蹤一下,發現調用的是caboutconstructer(caboutconstructer &rvalue),至此,我們可以确定一點,const修飾符在拷貝構造函數中對參數确實産生了影響,這是和普通函數不同的。

繼續擴大拷貝構造函數範圍,發現隻要是有const修飾的都沒有問題,更進一步表明const确實對拷貝構造函數的參數産生了影響。

用第二種方式

進行測試,發現這種方式調用的是caboutconstructer(const caboutconstructer &rvalue)拷貝構造函數。

哈哈,至此我們可以在實際應用中得到拷貝構造函數的應用例子了。

那麼再接下來的測試中,我們發現caboutconstructer不使用const修改的拷貝構造函數都也沒有問題,但是問題還沒有完。

    我們使用第一,第二中形式測試,發現隻要存在任意一對拷貝構造函數,都可以測試通過,為了便于說明,我們分别給他們編号,我們任意一對奇偶編号組合的拷貝構造函數都可以同時存在,并且可以執行相應的拷貝構造函數。

1)caboutconstructer(caboutconstructer &rvalue);

2)caboutconstructer(const caboutconstructer &rvalue);

3)caboutconstructer(caboutconstructer& rvalue,int ipara = 0);

4)caboutconstructer(const caboutconstructer& rvalue,int ipara = 0);

5)caboutconstructer(caboutconstructer& rvalue,int ipara1 = 0,int ipara2 = 0);

6)caboutconstructer(const caboutconstructer& rvalue,int ipara = 0,int ipara2 = 0);

那麼我們繼續用第三種形式測試

// 第三種形式

很遺憾,在指派運算中,不會調用拷貝構造函數。

用第四種方式進行測試,

// 第四種形式

發現調用的是caboutconstructer(caboutconstructer &rvalue)這個拷貝構造函數,很正常,因為a1是非const類型的,是以當然會調用非const的構造函數,那麼我們預測第五種形式,應該是調用

caboutconstructer(const caboutconstructer &rvalue);

第五種形式測試

不出所料果然是caboutconstructer(const caboutconstructer &rvalue);

至此,我們還沒有對函數傳回對象,異常抛出對象時的拷貝構造進行測試,不過做這麼多測試,我們可以預測,那兩種情況下拷貝構造函數的調用,應該是和普遍函數調用是相同。如果您不相信可以測試一下,如果是預測錯誤,歡迎您批評指正。

總結:構造函數,拷貝構造函數,析構函數由于其本身是特殊函數,雖然他們也遵守一般函數的一般規則,比方說存在函數重載,函數參數預設值,引用const的問題,但是并不是完全相同,比如他們沒有傳回值。而其自身又有很多特殊型,比方說explicit修飾符,對象初始化清單。

以上測試結果基于編譯環境。

編譯環境:windows2003 + vs2003

備注:以上測試,我們沒有考慮代碼優化,編譯器設定等方面,隻是着重考察c++的語言特性,如果您有什麼不滿的地方,歡迎指正。同時如果您在其他編譯器上做測試,測試結果與vc2003下不同,也希望您發送給我一份,注明您的編譯環境和編譯器版本,我将在修訂版中,署上您的大名以及測試結果。

最新修訂請到http://www.exuetang.net和http://blog.csdn.net/ugg查閱

聯系方式

msn:[email protected]

郵箱:[email protected]

這裡特别感謝csdn上的sinall網友,他首先指正了我對拷貝構造函數下const結論的問題

參考資料:

csdn:有關拷貝構造函數的說法不正确的是

http://community.csdn.net/expert/topicview3.asp?id=4720584

c++标準iso/iec 14882:2003(e)

深度探索c++對象模型(inside the c++ object model)

繼續閱讀