淺析拷貝構造函數
這篇文章,主要是受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 {
// ...
public:
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:
class X {
// ...
public:
X(const X&);
X(X&); //OK
};
現在總結一下:構造函數是一種特殊函數,而拷貝構造函數是一種特殊的構造函數,拷貝構造函數的第一個參數必須為type X&,或者Type const X&,要麼不存在其他參數,如果存在其他參數,其他參數必須有默認值,不妨加一句,根據這些定義可以確定一個類中可以有多個拷貝構造函數,但是我們根據拷貝構造函數的應用,即在賦值對象操作,對象作為參數時傳遞,以及對象作為返回值返回和在異常中拋出對象時,都需要調用類的拷貝構造函數生成對象這一點來定義拷貝構造函數,那麼類中是否還可以定義多個拷貝構造函數,即理論上可以,實際中是否也可以定義多個拷貝構造函數?這個問題我們先保留,稍後討論,
構造函數,拷貝構造函數都沒有返回值,如果程序沒有顯示聲明或者顯示聲明錯誤(ill -formed),都會生成相應的默認構造函數,拷貝構造函數,析構函數等。這些工作都有我們的編譯器在編譯的時候幫我們做好。
看過這些標準後,我的感覺就是C++難學,而C++編譯器更難做,因為編譯器要幫我們做太多的事情。這些都是題外話,我們將繼續我們的構造函數之旅。
也就是因為構造函數的特殊性(沒有函數名,不具有返回值,編譯器可以默認創建,一般函數可享受不了這種待遇,這還不特殊嗎),作為特殊函數,那麼必須尤其特殊性,才能彰顯出與眾不同。
1:explicit關鍵字就是為構造函數準備的。這個關鍵字的含義就是顯示調用構造函數,禁止編譯器轉換。確實編譯器幫我們做太多的轉換了,有時編譯器的這種好意會給我們帶來麻煩,所以我們要控製編譯器。
class CAboutConstructer
{
public:
explicit CAboutConstructer(int ivalue)
{
_iTemp = ivalue;
cout << "Constuctor" << endl;
}
inline void p_Show() const
{
cout << _iTemp << endl;
}
private:
int _iTemp;
};
CAboutConstructer a(1);
a.p_Show();
a = 6; // 如果CAboutConstructer聲明為explicit,那麼此處無法編譯,我們需要,顯示調用CAboutConstructer,聲明如下a = CAboutConstructer(6);
2:對象初始化列表,參考下麵構造函數中對_iTemp,不同的初始化方式。
class CAboutConstructer
{
public:
CAboutConstructer(int ivalue):_iTemp(ivalue)
{
// _iTemp = ivalue;
cout << "Constuctor" << endl;
}
private:
int _iTemp;
};
對象的構造過程是,類的構造函數首先調用類內變量的構造函數(在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的含義寫的有點不明白
“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”
“function”: 對重載函數的調用不明確
未能解析指定的重載函數調用。可能需要顯式轉換一個或多個實際參數。
那麼從上麵我們可以得出一點,編譯器隻有在使用函數時,才會對函數進行二義性檢查,或者說實現時。
看到這裏不得不想,拷貝構造函數會不會也這樣那?聲明時沒有問題,而在實際應用過程中出錯。
那麼我們做如下測試
// 第一種形式
CAboutConstructer a(1);
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
{
rValue.p_Show();
}
編譯出錯
error C2535: “void CAboutConstructer::p_Show(CAboutConstructer) const” : 已經定義或聲明成員函數。
這說明在按值傳遞的函數重載時,不能通過對一個參數添加const來實現函數重載,但是修改把上麵參數傳遞修改為引用方式,就可以通過添加const來實現函數重載,這些都是題外話,畢竟我們在這裏要測試的是,參數按值傳遞時,調用拷貝構造函數的問題,不知道這條規則是否也適合拷貝構造函數?拷貝構造函數是按引用方式傳遞。這一點是和普通函數調用方式一樣。
void p_Show(CAboutConstructer rValue) const
{
rValue.p_Show();
}
編譯正確。
為了驗證拷貝構造函數,現在我們也把拷貝構造函數實現,如下
CAboutConstructer(CAboutConstructer &rValue)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(const CAboutConstructer &rValue)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(CAboutConstructer& rValue,int ipara = 0)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(CAboutConstructer& rValue,int ipara1 = 0,int ipara2 = 0)
{
_iTemp = rValue._iTemp;
}
CAboutConstructer(const CAboutConstructer& rValue,int ipara = 0,int ipara2 = 0)
{
_iTemp = rValue._iTemp;
}
編譯通過,期待中的二義性還是沒有出現。
好,現在開始測試第一種情況
CAboutConstructer a(1);
CAboutConstructer a1(a);
編譯,期待中的二義性終於出現了
error C2668: “CAboutConstructer::CAboutConstructer” : 對重載函數的調用不明確可能是“CAboutConstructer::CAboutConstructer(CAboutConstructer &,int,int)” 或“CAboutConstructer::CAboutConstructer(CAboutConstructer &,int)”或“CAboutConstructer::CAboutConstructer(CAboutConstructer &)”
好那麼先注釋掉一些,僅保留
CAboutConstructer(CAboutConstructer &rValue)
測試沒有問題,繼續擴大拷貝構造函數範圍,加上
CAboutConstructer(const CAboutConstructer &rValue)
編譯通過,居然沒有問題,也沒有二義性。但是此時
CAboutConstructer a1(a);
究竟調用的那個拷貝構造函數那?我們跟蹤一下,發現調用的是CAboutConstructer(CAboutConstructer &rValue),至此,我們可以確定一點,const修飾符在拷貝構造函數中對參數確實產生了影響,這是和普通函數不同的。
繼續擴大拷貝構造函數範圍,發現隻要是有const修飾的都沒有問題,更進一步表明const確實對拷貝構造函數的參數產生了影響。
用第二種方式
const CAboutConstructer b(1);
CAboutConstructer b1(b);
進行測試,發現這種方式調用的是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);
那麼我們繼續用第三種形式測試
// 第三種形式
b1 = a1;
很遺憾,在賦值運算中,不會調用拷貝構造函數。
用第四種方式進行測試,
// 第四種形式
b1.p_Show(a1);
發現調用的是CAboutConstructer(CAboutConstructer &rValue)這個拷貝構造函數,很正常,因為a1是非const類型的,所以當然會調用非const的構造函數,那麼我們預測第五種形式,應該是調用
CAboutConstructer(const CAboutConstructer &rValue);
第五種形式測試
a1.p_Show(b);
不出所料果然是CAboutConstructer(const CAboutConstructer &rValue);
至此,我們還沒有對函數返回對象,異常拋出對象時的拷貝構造進行測試,不過做這麼多測試,我們可以預測,那兩種情況下拷貝構造函數的調用,應該是和普遍函數調用是相同。如果您不相信可以測試一下,如果是預測錯誤,歡迎您批評指正。
總結:構造函數,拷貝構造函數,析構函數由於其本身是特殊函數,雖然他們也遵守一般函數的一般規則,比方說存在函數重載,函數參數默認值,引用const的問題,但是並不是完全相同,比如他們沒有返回值。而其自身又有很多特殊型,比方說explicit修飾符,對象初始化列表。
以上測試結果基於編譯環境。
編譯環境:Windows2003 + VS2003
備注:以上測試,我們沒有考慮代碼優化,編譯器設置等方麵,隻是著重考察C++的語言特性,如果您有什麼不滿的地方,歡迎指正。同時如果您在其他編譯器上做測試,測試結果與VC2003下不同,也希望您發送給我一份,注明您的編譯環境和編譯器版本,我將在修訂版中,署上您的大名以及測試結果。
最新修訂請到https://www.exuetang.net和https://blog.csdn.net/ugg查閱
聯係方式
郵箱:exuetang@163.com
這裏特別感謝CSDN上的sinall網友,他首先指正了我對拷貝構造函數下const結論的問題
參考資料:
CSDN:有關拷貝構造函數的說法不正確的是
https://community.csdn.net/Expert/TopicView3.asp?id=4720584
C++標準ISO/IEC 14882:2003(E)
深度探索C++對象模型(Inside The C++ Object Model)
最後更新:2017-04-02 00:00:27