序:對於一個空類,編譯器默認生成四個成員函數:默認構造函數、析構函數、拷貝構造函數、賦值函數
一,默認構造函數
默認構造函數(default constructor)就是在沒有顯式提供初始化式時調用的構造函數。它由不帶參數的構造函數,或者為所有的形參提供默認實參的構造函數定義。如果定義某個類的變量時沒有提供初始化式就會使用默認構造函數。
如果用戶定義的類中沒有顯式的定義任何構造函數,編譯器就會自動為該類型生成默認構造函數,稱為合成的構造函數(synthesized default constructor)。
C++語言為類提供的構造函數可自動完成對象的初始化任務
全局對象和靜態對象的構造函數在main()函數執行之前就被調用,局部靜態對象的構造函數是當程序第一次執行到相應語句時才被調用。然而給出一個外部對象的引用性聲明時,並不調用相應的構造函數,因為這個外部對象只是引用在其他地方聲明的對象,並沒有真正地創建一個對象。
C++的構造函數定義格式為:
class <類名>
{
public: <類名>(參數表) //...(還可以聲明其它成員函數)
};
<類名>::<函數名>(參數表)
{ //函數體 }
如以下定義是合法的:
class T
{
public: T(int a=0){i=a;}//構造函數允許直接寫在類定義內,也允許有參數表。
private:int i;
};
二,析構函數
當程序員沒有給類創建析構函數,那麼系統會在類中自動創建一個析構函數,形式為:~A(){},為類A創建的析構函數。當程序執行完後,系統自動調用自動創建的析構函數,將對象釋放。
默認的析構函數不能刪除new運算符在自由存儲器中分配的對象或對象成員。如果類成員占用的空間是在構造函數中動態分配的,我們就必須自定義析構函數,然後顯式使用delete運算符來釋放構造函數使用new運算符分配的內存,就像銷毀普通變量一樣
#include <iostream>
using namespace std;
class Pig
{
public:
Pig()
{
cout < < "Pig constructed " < <endl;
}
~Pig()
{
cout < < "Pig destructed " < <endl;
}
};
class Japanese:Pig
{
};
int main()
{
Japanese dog;
return 0;
}
輸出:
Pig constructed
Pig destructed
如果改成一下new 生成的對象則不調用默認析構函數
int main()
{
Japanese *dog=new Japanese;
return 0;
}
輸出就只有:
Pig constructed
三,拷貝構造函數
CExample(const CExample&); //參數是const 對象的引用&
【注意】如果不主動編寫拷貝構造函數和賦值函數,編譯器將以“位拷貝”的方式自動生成缺省的函數。倘若類中含有指針變量,那麼這兩個缺省的函數就隱含了錯誤。
以類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被釋放了兩次。
1)默認拷貝構造函數
對於普通類型的對象來說,它們之間的復制是很簡單的,例如:
int a=88;
int b=a; //復制
而類對象與普通對象不同,類對象內部結構一般較為復雜,存在各種成員變量。
下面看一個類對象拷貝的簡單例子。
#include <iostream>
using namespace std;
class CExample {
private:
int a;
public:
CExample(int b)
{ a=b;}
void Show ()
{
cout<<a<<endl;
}
};
int main()
{
CExample A(100);
CExample B=A;
B.Show ();
return 0;
}
運行程序,屏幕輸出100。
系統為對象B分配了內存並完成了與對象A的復制過程。就類對象而言,相同類型的類對象是通過拷貝構造函數來完成整個復制過程的。下面舉例說明拷貝構造函數的工作過程。
2)顯式拷貝構造函數
#include <iostream>
using namespace std;
class CExample {
private:
int a;
public:
CExample(int b)
{ a=b;}
CExample(const CExample& C)//拷貝構造函數
{
a=C.a;
}
void Show ()
{
cout<<a<<endl;
}
};
int main()
{
CExample A(100);
CExample B=A;
B.Show ();
return 0;
}
CExample(constCExample& C)就是我們自定義的拷貝構造函數。可見,拷貝構造函數是一種特殊的構造函數,函數的名稱必須和類名稱一致,它的唯一的一個參數是本類型的一個引用變量,該參數是const類型,不可變的。例如:類X的拷貝構造函數的形式為X(X& x)。
當用一個已初始化過了的對象去初始化另一個新構造的對象的時候,拷貝構造函數就會被自動調用。也就是說,當類的對象需要拷貝時,拷貝構造函數將會被調用。以下情況都會調用拷貝構造函數:
一個對象以值傳遞的方式傳入函數體
一個對象以值傳遞的方式從函數返回
一個對象需要通過另外一個對象進行初始化。
如果在類中沒有顯式地聲明一個拷貝構造函數,那麼,編譯器將會自動生成一個默認的拷貝構造函數,該構造函數完成對象之間的位拷貝。位拷貝又稱淺拷貝,後面將進行說明。
淺拷貝和深拷貝
在某些狀況下,類內成員變量需要動態開辟堆內存,如果實行位拷貝,也就是把對象裡的值完全復制給另一個對象,如A=B。這時,如果B中有一個成員變量指針已經申請了內存,那A中的那個成員變量也指向同一塊內存。這就出現了問題:當B把內存釋放了(如:析構),這時A內的指針就是野指針了,出現運行錯誤。
深拷貝和淺拷貝可以簡單理解為:如果一個類擁有資源,當這個類的對象發生復制過程的時候,資源重新分配,這個過程就是深拷貝,反之,沒有重新分配資源,就是淺拷貝。
3)深拷貝 (主要應對類中有指針變量的情況)
#include <iostream>
using namespace std;
class CA
{
public:
CA(int b,char* cstr)
{
a=b;
str=new char[b];
strcpy(str,cstr);
}
CA(const CA& C)
{
a=C.a;
str=new char[a]; //深拷貝
if(str!=0)
strcpy(str,C.str);
}
void Show()
{
cout<<str<<endl;
}
~CA()
{
delete str;
}
private:
int a;
char *str;
};
int main()
{
CA A(10,"Hello!");
CA B=A;
B.Show();
return 0;
}
深拷貝:類擁有資源(堆,或者是其它系統資源),當這個類的對象發生復制過程的時候
淺拷貝:對象存在資源,但復制過程並未復制資源的情況視為淺拷貝。
淺拷貝缺點:淺拷貝資源後在釋放資源的時候會產生資源歸屬不清的情況導致程序運行出錯。
Test(Test &c_t)是自定義的拷貝構造函數,拷貝構造函數的名稱必須與類名稱一致,函數的形式參數是本類型的一個引用變量,且必須是引用。
當用一個已經初始化過了的自定義類類型對象去初始化另一個新構造的對象的時候,拷貝構造函數就會被自動調用,如果你沒有自定義拷貝構造函數的時候,系統將會提供給一個默認的拷貝構造函數來完成這個過程,上面代碼的復制核心語句就是通過Test(Test &c_t)拷貝構造函數內的p1=c_t.p1;語句完成的。
四,賦值函數
每個類只有一個賦值函數
由於並非所有的對象都會使用拷貝構造函數和賦值函數,程序員可能對這兩個函數有些輕視。
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被釋放了兩次。
2,拷貝構造函數和賦值函數非常容易混淆,常導致錯寫、錯用。拷貝構造函數是在對象被創建時調用的,而賦值函數只能被已經存在了的對象調用。以下程序中,第三個語句和第四個語句很相似,你分得清楚哪個調用了拷貝構造函數,哪個調用了賦值函數嗎?
String a(“hello”);
String b(“world”);
String c = a; // 調用了拷貝構造函數,最好寫成 c(a);
c = b; // 調用了賦值函數
本例中第三個語句的風格較差,宜改寫成String c(a) 以區別於第四個語句。
類String的拷貝構造函數與賦值函數
// 拷貝構造函數
String::String(const String &other)
{
// 允許操作other的私有成員m_data
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;
}
類String拷貝構造函數與普通構造函數的區別是:在函數入口處無需與NULL進行比較,這是因為“引用”不可能是NULL,而“指針”可以為NULL。
類String的賦值函數比構造函數復雜得多,分四步實現:
(1)第一步,檢查自賦值。你可能會認為多此一舉,難道有人會愚蠢到寫出 a = a 這樣的自賦值語句!的確不會。但是間接的自賦值仍有可能出現,例如 // 內容自賦值 b = a; … c = b; … a = c; // 地址自賦值 b = &a; … a = *b; 也許有人會說:“即使出現自賦值,我也可以不理睬,大不了化點時間讓對象復制自己而已,反正不會出錯!” 他真的說錯了。看看第二步的delete,自殺後還能復制自己嗎?所以,如果發現自賦值,應該馬上終止函數。注意不要將檢查自賦值的if語句 if(this == &other) 錯寫成為 if( *this == other)
(2)第二步,用delete釋放原有的內存資源。如果現在不釋放,以後就沒機會了,將造成內存洩露。
(3)第三步,分配新的內存資源,並復制字符串。注意函數strlen返回的是有效字符串長度,不包含結束符‘\0’。函數strcpy則連‘\0’一起復制。
(4)第四步,返回本對象的引用,目的是為了實現象 a = b = c 這樣的鏈式表達。注意不要將 return *this 錯寫成 return this 。那麼能否寫成return other 呢?效果不是一樣嗎? 不可以!因為我們不知道參數other的生命期。有可能other是個臨時對象,在賦值結束後它馬上消失,那麼return other返回的將是垃圾。 偷懶的辦法處理拷貝構造函數與賦值函數 如果我們實在不想編寫拷貝構造函數和賦值函數,又不允許別人使用編譯器生成的缺省函數,怎麼辦?
偷懶的辦法是:只需將拷貝構造函數和賦值函數聲明為私有函數,不用編寫代碼。
例如:
class A { …
private: A(const A &a); // 私有的拷貝構造函數
A & operate =(const A &a); // 私有的賦值函數
};
如果有人試圖編寫如下程序:
A b(a); // 調用了私有的拷貝構造函數
b = a; // 調用了私有的賦值函數
編譯器將指出錯誤,因為外界不可以操作A的私有函數。 注意:以上例子在vc中可能編譯不過,因關鍵字不是operate ,而是operator
3.在編寫派生類的賦值函數時,注意不要忘記對基類的數據成員重新賦值.
作者:tianshuai11