第九章 類的建構函式、解構函式與賦值函數
類對象之間的賦值只是對資料成員賦值。
每個類只有一個解構函式和一個賦值函數,但可有多個建構函式(包含一個拷貝建構函式,其他的稱為普通建構函式)。這幾個函數都不被繼承。對任意一個類A,若不想編寫上述函數,C++編譯器將自動為A產生四個預設的函數。有預設的,為什麼還要程式員編寫:
1)若使用“預設的無參建構函式”和“預設的解構函式”,等於放棄了自主“初始化”和“清除”的機會。
2)“預設的拷貝建構函式”和“預設的賦值函數”均採用“位拷貝”而非“值拷貝”的方式來實現,若類中含有指標變數,這兩個函數將出錯。
class String
{
public:
String(const char *str = NULL); //普通建構函式
String(const String &other); //拷貝建構函式
~String(void); //解構函式
String & operator =(const String &other); //賦值函數
private:
char *m_data; //用於儲存字串
}
9.1建構函式的初始化表
建構函式特殊在其初始化方式(初始化表和函數體內賦值兩種方式)和執行時間。
建構函式有個特殊的初始化方式叫“初始設定式表”(簡稱初始化表)。初始化表位於函數參數後,卻在函數體{}前,說明該表裡的初始化工作發生在函數體內的任何代碼被執行之前。建構函式的初始化表使用規則:
1)
若類存在繼承關係,衍生類別必須在其初始化表裡調用基類的建構函式。如:
B::B(int x, int y) : A(x) {……}//初始化表裡調用A的建構函式。若基類含無參建構函式,則不必在衍生類別的初始化表裡調用基類建構函式,但構造衍生類別時會調用基類的無參建構函式。所以最好還是寫上。
2)類的const常量只能在初始化表裡被初始化,因為它不能在函數體內用賦值的方式來初始化。
3)類的資料成員的初始化可以採用初始化表或函數體內賦值兩種方式,這兩種方式的效率不完全相同。非內部資料類型的成員對象應採用第一種方式初始化,以擷取更高的效率。對內部資料類型,兩者幾乎無差別。
B::B(const A &a) : m_a(a){…}類B的建構函式在其初始化表裡調用類A的拷貝建構函式,將成員對象m_a初始化。B::B(const
A &a){m_a=a;}先建立m_a對象,再調用A的賦值函數,將參數a賦給m_a。
9.2構造和析構的次序
構造從類層次的最根處開始,在每層中,先調用基類的建構函式,然後調用成員對象的建構函式。析構則嚴格按照與構造相反的次序進行。
成員對象的初始化次序不受它們在初始化表中次序的影響,只由成員對象在類中聲明的次序決定。
9.3樣本:類String的建構函式與解構函式
//String的普通建構函式
String::String(cosnt char *str)
{
if (NULL == str)
{
m_data = new char[1];
*m_data = ‘\0’;
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
//String的解構函式
String::~String(void)
{
delete [] m_data;//因m_data是內部資料類型,也可寫成delete m_data。
}
9.4不要輕視拷貝建構函式與賦值函數
1)不主動編寫拷貝建構函式和賦值函數,編譯器將以“位拷貝”的方式自動產生預設的函數。若類中有指標變數,那麼這兩個預設的函數就隱含了錯誤。“位拷貝”又稱“淺拷貝”,拷貝的是地址;而“值拷貝”又稱“深拷貝”,拷貝的是內容。以類String的兩個對象a,b為例,假設a.m_data的內容為“hello”,b.m_data的內容為“world”。
將a賦值給b,若預設賦值函數,則執行的是“位拷貝”:b.m_data = a.m_data。這將造成三個錯誤:
Ø
b.m_data原有的記憶體沒被釋放,造成記憶體泄露。
Ø
b.m_data和a.m_data執行同一記憶體,a或b任一方改變都會影響第三方。
Ø
在對象被析構時,m_data被釋放兩次。
int *p = new int;int *q=p; ---------淺拷貝
int *p = new int;int *q = new int;*p = *q; --------深拷貝
2)拷貝建構函式和賦值函數易混淆。區分:拷貝建構函式是在對象被建立時調用的,而賦值函數只能被已經存在了的對象調用。試著區分以下語句:
String a(“Hello”); String b(“World”);
String c = a; //調用了拷貝建構函式,最後寫成c(a);
C = b; //調用了賦值函數。
9.5樣本:類String的拷貝建構函式與賦值函數
//拷貝建構函式
String::String(const String &other)
{
//允許操作other的私人成員
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
//賦值函數
String &String::operator =(const String &other)
{
//1)檢查自賦值
if (this==&other)
return *this;
//2)釋放原有的記憶體資源
delete []m_data;
//3)分配新的記憶體資源,並複製內容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
//4)返回本對象的引用
return *this;
}
1)第一步,檢查自賦值。有人可能會寫出間接指派陳述式,如:
//內容自賦值 //地址自賦值
b = a; b = &a;
…… ……
c = b; a = *b;
……
a = c;
為何要避免出現自賦值,因第二部要delete,自殺後還能複製自己嗎?為何要delete,怕原來分配的記憶體不夠用,第三步分配足夠記憶體。注意不要將檢查自賦值的if語句if(this==&ohter)寫成if(*this==other)。
2)第二步,釋放原記憶體,若不釋放,將會造成記憶體泄露。
3)第三步,分配新記憶體並分配字串。注意strlen返回的是有效字串長度,不包含’\0’。
4)第四步,返回本對象的引用,目的是為了實現像a=b=c這樣的鏈式運算式。注意不要將return *this寫成return this。
9.6偷懶的辦法處理拷貝建構函式與賦值函數
若不想編寫拷貝建構函式與賦值函數,又不允許別人使用編譯器產生的預設函數,偷懶的方法是:將建構函式與賦值函式宣告為私人函數,不用編寫代碼。若有人試圖調用這倆函數,編譯器將指出錯誤。
9.7如何在衍生類別中實作類別的基本函數(即構造、析構、賦值函數)
基類的構造、析構和賦值函數不被繼承。若類之間存在繼承關係,如何編寫這些函數:
Ø
衍生類別的建構函式應在其初始化表裡調用基類的建構函式。
Ø
基類的解構函式應為virtual,即覆蓋,否則會被隱藏。最好將衍生類別解構函式也設為virtual,以便後續的再繼承。
Ø
在編寫衍生類別的賦值函數時,要對基類的資料成員重新賦值。