C/C++ 類預設生成的四個函式
序:對於一個空類,編譯器預設生成四個成員函式:預設建構函式、解構函式、拷貝建構函式、賦值函式
一,預設建構函式
預設建構函式(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.在編寫派生類的賦值函式時,注意不要忘記對基類的資料成員重新賦值.
轉載地址: http://blog.csdn.net/tianshuai1111/article/details/7779701