class Empty
{
public:
Empty(); //缺省構造函數
Empty(const Empty&); //拷貝構造函數
~Empty(); //析構函數
Empty& operator=(const Empty&); //賦值運算符
Empty* operator&(); //取值運算符
const Empty* operator&() const; // 取值運算符
};
例如有以下class:
class StringBad
{
private :
char * str;
int len;
public :
StringBad( const char * s);
StringBad();
~ StringBad();
} ;
在構造函數和析構函數定義當中有如下定義:
StringBad::StringBad( const char * s)
{
len = std::strlen(s);
str = new char [len + 1 ];
}
StringBad::StringBad()
{
len = 4 ;
str = new char [ 4 ];
}
StringBad:: ~ StringBad()
{
delete [] str;
}
那麼在程序當中如果有以下代碼:
StringBad sports( " Spinach Leaves Bow1 for bollars " );
StringBad sailor = sports;
以上的第二條初始化語句將會調用什麼構造函數?記住,這種形式的初始化等效於下面的語句:
StringBad sailor = StringBad(sports);
因為sports的類型為StringBad,因此相應的構造函數原型應該如下:
StringBad( const StringBad & );
當我們使用一個對象來初始化另一個對象時,編譯器將自動生成上述構造函數(稱為復制構造函數,因為它創建對象的一個副本)。
現在我們來看看我們沒有定義復制構造函數的情況下調用隱式復制構造函數將會出現什麼情況。
從構造函數定義的代碼片斷可以看到,當中使用new操作符初始化了一個指針str,而隱式的復制構造函數是按值進行復制的,那麼對於指針str,將會進行如下復制:
sailor.str = sports.str;
這裡復制的不是字符串,而是一個指向字符串的指針!也就是說,我們將得到兩個指向同一個字符串的指針!由此會產生的問題將不言而喻。當其中一個對象調用了析構函數之後,其str指向的內存將被釋放,這個時候我們如果調用另一個對象,其str指向的地址數據會是什麼?很明顯將會出現不可預料的結果。
所以由此可見,如果類中包含了使用new初始化的指針成員,應當定義一個復制構造函數,以復制指向的數據,而不是指針,這被稱為深度復制。因為默認的淺復制(或成為成員復制)僅淺淺的賦值指針信息。
我們再看以下代碼片斷,我們稍做修改:
StringBad headline1( " Celery Stalks at Midnight " );
StringBad knot;
knot = headline1;
這裡的最後一行將與以上例子有所區別,現在是將已有對象賦給另一個已有對象,這將會采取其他操作,即使用重載的賦值操作符。(我們需要知道的是:初始化總是會調用復制構造函數,而使用=操作符時也可能調用賦值操作符)因為C++允許對象賦值,這是通過自動為類重載賦值操作符實現的。其原型如下:
Class_name & Class_name:: operator = ( const Class_name & );
它接受並返回一個指向類對象的引用。
與隱式的復制構造函數一樣,隱式的對象賦值操作符也會產生同樣的問題,即包含了使用new初始化的指針成員時,只會采用淺復制。所以我們需要使用同樣的解決辦法,即定義一個重載的賦值操作符來實現深度復制。
所以綜上所述,如果類中包含了使用new初始化的指針成員,我們應該顯式定義一個復制構造函數和一個重載的賦值操作符來實現其深度復制,避免由此帶來的成員復制問題
1. 以下函數哪個是拷貝構造函數,為什麼?
X::X(const X&);
X::X(X);
X::X(X&, int a=1);
X::X(X&, int a=1, b=2);
2. 一個類中可以存在多於一個的拷貝構造函數嗎?
3. 寫出以下程序段的輸出結果, 並說明為什麼? 如果你都能回答無誤的話,那麼你已經對拷貝構造函數有了相當的了解。
#include <iostream></iostream>
#include <string></string>
struct X {
template<typename T>
X( T& ) { std::cout << "This is ctor." << std::endl; }
template<typename T>
X& operator=( T& ) { std::cout << "This is ctor." << std::endl; }
};
void main() {
X a(5);
X b(10.5);
X c = a;
c = b;
}
解答如下:
1. 對於一個類X,如果一個構造函數的第一個參數是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且沒有其他參數或其他參數都有默認值,那麼這個函數是拷貝構造函數。
X::X(const X&); //是拷貝構造函數
X::X(X&, int=1); //是拷貝構造函數
2.類中可以存在超過一個拷貝構造函數,
class X {
public:
X(const X&);
X(X&); // OK
};
注意,如果一個類中只存在一個參數為X&的拷貝構造函數,那麼就不能使用const X或volatile X的對象實行拷貝初始化。
class X {
public:
X();
X(X&);
};
const X cx;
X x = cx; // error
如果一個類中沒有定義拷貝構造函數,那麼編譯器會自動產生一個默認的拷貝構造函數。
這個默認的參數可能為X::X(const X&)或X::X(X&),由編譯器根據上下文決定選擇哪一個。
默認拷貝構造函數的行為如下:
默認的拷貝構造函數執行的順序與其他用戶定義的構造函數相同,執行先父類後子類的構造。
拷貝構造函數對類中每一個數據成員執行成員拷貝(memberwise Copy)的動作。
a)如果數據成員為某一個類的實例,那麼調用此類的拷貝構造函數。
b)如果數據成員是一個數組,對數組的每一個執行按位拷貝。
c)如果數據成員是一個數量,如int,double,那麼調用系統內建的賦值運算符對其進行賦值。
3.拷貝構造函數不能由成員函數模版生成。
struct X {
template<typename T>
X( const T& ); // NOT copy ctor, T can't be X
template<typename T>
operator=( const T& ); // NOT copy ass't, T can't be X
};
原因很簡單, 成員函數模版並不改變語言的規則,而語言的規則說,如果程序需要一個拷貝構造函數而你沒有聲明它,那麼編譯器會為你自動生成一個。 所以成員函數模版並不會阻止編譯器生成拷貝構造函數, 賦值運算符重載也遵循同樣的規則。(參見Effective C++ 3edition, Item45)