找工作筆試面試那些事兒(5)---建構函式、解構函式和賦值函式
作者:寒小陽
時間:2013年9月。
出處:http://blog.csdn.net/han_xiaoyang/article/details/10833931。
宣告:版權所有,轉載請註明出處,謝謝。
類的建構函式、解構函式與賦值函式
學過C++的人都說自己知道建構函式、解構函式與賦值函式。它們看似太普通,以致讓人容易麻痺大意,但請你認真讀完這部分內容,你會發現這些貌似簡單的函式實則像沒有頂蓋的下水道那樣危險。
每個類只有一個解構函式和一個賦值函式,但可以有多個建構函式(包含一個拷貝建構函式,其它的稱為普通建構函式)。對於任意一個類A,如果不想編寫上述函式,C++編譯器將自動為A產生四個預設的函式,如
A(void); // 預設的無引數建構函式
A(const A &a); // 預設的拷貝建構函式
~A(void); // 預設的解構函式
A & operate =(const A &a); // 預設的賦值函式
這不禁讓人疑惑,既然能自動生成函式,為什麼還要程式設計師編寫?
原因如下:
(1)如果使用“預設的無引數建構函式”和“預設的解構函式”,等於放棄了自主“初始化”和“清除”的機會,C++發明人Stroustrup的好心好意白費了。
(2)“預設的拷貝建構函式”和“預設的賦值函式”均採用“位拷貝”而非“值拷貝”的方式來實現,倘若類中含有指標變數,這兩個函式註定將出錯。
這部分內容中以類String 的設計與實現為例,深入闡述被很多教科書忽視了的道理。事實上,這個例子至今仍然被很多公司哪來做筆試面試題,去年筆試的時候遇到過好幾次,希望大家能從中得到一點幫助。String的結構如下:
class String
{
public:
String(const char *str = NULL); // 普通建構函式
String(const String &other); // 拷貝建構函式
~ String(void); // 解構函式
String & operate =(const String &other); // 賦值函式
private:
char *m_data; // 用於儲存字串
}; 。
1 建構函式與解構函式初步
作為比C更先進的語言,C++提供了更好的機制來增強程式的安全性。C++編譯器具有嚴格的型別安全檢查功能,它幾乎能找出程式中所有的語法問題,這的確幫了程式設計師的大忙。現在比較容易犯的是級別高的錯誤,它們通常隱藏得很深。而筆試面試中考察的也多是這些錯誤,而其中和構造解構函式相關的問題不少。
不少難以察覺的程式錯誤是由於變數沒有被正確初始化或清除造成的,而初始化和清除工作很容易被人遺忘。Stroustrup在設計C++語言時充分考慮了這個問題並很好地予以解決:把物件的初始化工作放在建構函式中,把清除工作放在解構函式中。當物件被建立時,建構函式被自動執行。當物件消亡時,解構函式被自動執行。這下就不用擔心忘了物件的初始化和清除工作。
建構函式與解構函式的名字不能隨便起,必須讓編譯器認得出才可以被自動執行。建構函式、解構函式與類同名,由於解構函式的目的與建構函式的相反,就加字首‘~’以示區別。
除了名字外,建構函式與解構函式的另一個特別之處是沒有返回值型別,這與返回值型別為void的函式不同。建構函式與解構函式的使命非常明確,就象出生與死亡,光溜溜地來光溜溜地去。如果它們有返回值型別,那麼編譯器將不知所措。為了防止節外生枝,乾脆規定沒有返回值型別。
2 建構函式的初始化表
這是經常出的考點以至於要單獨拿出一節來提一提。
建構函式有個特殊的初始化方式叫“初始化表示式表”(簡稱初始化表)。初始化表位於函式引數表之後,卻在函式體{}之前。這說明該表裡的初始化工作發生在函式體內的任何程式碼被執行之前。
以下是建構函式初始化表的使用規則和注意點:
1)如果類存在繼承關係,派生類必須在其初始化表裡呼叫基類的建構函式。
例如
class A
{…
A(int x); // A的建構函式
};
class B : public A
{…
B(int x, int y);// B的建構函式
};
B::B(int x, int y)
: A(x) // 在初始化表裡呼叫A的建構函式
{
…
}
2)類的const 常量只能在初始化表裡被初始化,因為它不能在函式體內用賦值的方式來初始化。
3)類的資料成員的初始化可以採用初始化表或函式體內賦值兩種方式,這兩種方式的效率不完全相同。
非內部資料型別的成員物件應當採用第一種方式初始化,以獲取更高的效率。例如
class A
{…
A(void); // 無引數建構函式
A(const A &other); // 拷貝建構函式
A & operate =( const A &other); // 賦值函式
};
class B
{
public:
B(const A &a); // B的建構函式
private:
A m_a; // 成員物件
};
例下圖左,類B的建構函式在其初始化表裡呼叫了類A的拷貝建構函式,從而將成員物件m_a初始化。
下圖右中,類B的建構函式在函式體內用賦值的方式將成員物件m_a初始化。我們看到的只是一條賦值語句,但實際上B的建構函式幹了兩件事:先暗地裡建立m_a物件(呼叫了A的無引數建構函式),再呼叫類A的賦值函式,將引數a賦給m_a。
對於內部資料型別的資料成員而言,兩種初始化方式的效率幾乎沒有區別,但後者的程式版式似乎更清晰些。若類F的宣告如下:
class F
{
public:
F(int x, int y); // 建構函式
private:
int m_x, m_y;
int m_i, m_j;
}
下圖左F的建構函式採用了第一種初始化方式,下圖右F的建構函式採用了第二種初始化方式。
3 構造和析構的次序
構造從類層次的最根處開始,在每一層中,首先呼叫基類的建構函式,然後呼叫成員物件的建構函式。析構則嚴格按照與構造相反的次序執行,該次序是唯一的,否則編譯器將無法自動執行析構過程。
一個有趣的現象是,成員物件初始化的次序完全不受它們在初始化表中次序的影響,只由成員物件在類中宣告的次序決定。這是因為類的宣告是唯一的,而類的建構函式可以有多個,因此會有多個不同次序的初始化表。如果成員物件按照初始化表的次序進行構造,這將導致解構函式無法得到唯一的逆序。
4 典型例子:類String的建構函式與解構函式
// String的普通建構函式
String::String(const char *str)
{
if(str==NULL)
{
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;
}
5 關於拷貝建構函式和賦值函式
由於並非所有的物件都會使用拷貝建構函式和賦值函式,程式設計師可能對這兩個函式有些輕視。請先記住以下的警告,在閱讀正文時就會多心:
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) 以區別於第四個語句。
6 示例:類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::operate =(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 這樣的自賦值語句!的確不會。但是間接的自賦值仍有可能出現,例如:
也許有人會說:“即使出現自賦值,我也可以不理睬,大不了化點時間讓物件複製自己而已,反正不會出錯!”
他真的說錯了。看看第二步的delete,自殺後還能複製自己嗎?所以,如果發現自賦值,應該馬上終止函式。注意不要將檢查自賦值的if語句
if(this == &other)
錯寫成為
if( *this == other)
(2)第二步,用delete釋放原有的記憶體資源。如果現在不釋放,以後就沒機會了,將造成記憶體洩露。
(3)第三步,分配新的記憶體資源,並複製字串。注意函式strlen返回的是有效字串長度,不包含結束符‘\0’。函式strcpy則連‘\0’一起復制。
(4)第四步,返回本物件的引用,目的是為了實現象a= b =c 這樣的鏈式表達。注意不要將return *this 錯寫成return this 。那麼能否寫成returnother 呢?效果不是一樣嗎?
不可以!因為我們不知道引數other的生命期。有可能other是個臨時物件,在賦值結束後它馬上消失,那麼return other返回的將是垃圾。
7 偷懶的辦法處理拷貝建構函式與賦值函式
如果我們實在不想編寫拷貝建構函式和賦值函式,又不允許別人使用編譯器生成的預設函式,怎麼辦?
偷懶的辦法是:只需將拷貝建構函式和賦值函式宣告為私有函式,不用編寫程式碼。
例如:
class A
{ …
private:
A(const A &a); // 私有的拷貝建構函式
A & operate =(const A &a); // 私有的賦值函式
};
如果有人試圖編寫如下程式:
A b(a); // 呼叫了私有的拷貝建構函式
b = a; // 呼叫了私有的賦值函式
編譯器將指出錯誤,因為外界不可以操作A的私有函式。
8 如何在派生類中實現類的基本函式
基類的建構函式、解構函式、賦值函式都不能被派生類繼承。如果類之間存在繼承關係,在編寫上述基本函式時應注意以下事項:
1)派生類的建構函式應在其初始化表裡呼叫基類的建構函式。
2)基類與派生類的解構函式應該為虛(即加virtual關鍵字)。例如
#include <iostream.h>
class Base
{
public:
virtual ~Base() { cout<< "~Base" << endl ; }
};
class Derived : public Base
{
public:
virtual ~Derived() { cout<< "~Derived" << endl ; }
};
void main(void)
{
Base * pB = new Derived; // upcast
delete pB;
}
輸出結果為:
~Derived
~Base
如果解構函式不為虛,那麼輸出結果為
~Base
3)在編寫派生類的賦值函式時,注意不要忘記對基類的資料成員重新賦值。例如:
class Base
{
public:
…
Base & operate =(const Base &other); // 類Base的賦值函式
private:
int m_i, m_j, m_k;
};
class Derived : public Base
{
public:
…
Derived & operate =(const Derived &other); // 類Derived的賦值函式
private:
int m_x, m_y, m_z;
};
Derived & Derived::operate =(const Derived &other)
{
//(1)檢查自賦值
if(this == &other)
return *this;
//(2)對基類的資料成員重新賦值
Base::operate =(other); // 因為不能直接操作私有資料成員
//(3)對派生類的資料成員賦值
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
//(4)返回本物件的引用
return *this;
}
---------------------
作者:寒小陽
來源:CSDN
原文:https://blog.csdn.net/han_xiaoyang/article/details/10833931?utm_source=copy
版權宣告:本文為博主原創文章,轉載請附上博文連結!