構造函數、析構函數與賦值函數是每個類最基本的函數,在一些公司的面試中也會經常問到這方面的問題。每個類只有一個析構函數和一個賦值函數,但可以有多個構造函數(包含一個拷貝構造函數,其它的稱爲普通構造函數)。對於任意一個類A,如果不手動編寫上述函數,C++編譯器將自動爲類A生成四個缺省的函數:
A(void); // 缺省的無參數構造函數
A(const A &a); // 缺省的拷貝構造函數
~A(void); // 缺省的析構函數
A& operate =(const A &a); // 缺省的賦值函數
雖然有自動生成,但是還是有必要手動寫上述函數的。因爲:
(1)如果使用“缺省的無參數構造函數”和“缺省的析構函數”,等於放棄了自主“初始化”和“清除”的機會,C++發明人Stroustrup的好心好意白費了。
(2)“缺省的拷貝構造函數”和“缺省的賦值函數”均採用“位拷貝”而非“值拷貝”的方式來實現,倘若類中含有指針變量,這兩個函數註定將出錯。
下面以類String的設計與實現爲例,深入探討這個道理。String的結構如下:
class String{
private:
char *m_data;//成員變量,用於保存字符串
public:
String(const char *str=NULL);//普通構造函數
String(const String &other);//拷貝構造函數
~String();//析構函數
String &operator=(const String &other);//賦值函數
};
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;
}
剛剛上面說,如果不主動編寫拷貝構造函數和賦值函數,編譯器將以“位拷貝”的方式自動生成缺省的函數。倘若類中含有指針變量,那麼這兩個缺省的函數就隱含了錯誤。以類String的兩個對象a,b爲例,假設a.m_data的內容爲“hello”,b.m_data的內容爲“world”。位拷貝拷貝的是地址,而值拷貝則拷貝的是內容。現將a賦給b,缺省賦值函數的“位拷貝”意味着執行b.m_data = a.m_data,雖然b.m_data所指向的內容會變成”hello”,但是這將造成三個錯誤:一是b.m_data原有的內存沒被釋放,造成內存泄露;二是b.m_data和a.m_data指向同一塊內存,a或b任何一方變動都會影響另一方;三是在對象被析構時,m_data被釋放了兩次。
對於編譯器,如果不主動編寫拷貝函數和賦值函數,它會以“位拷貝”的方式自動生成缺省的函數。如果重寫賦值函數和拷貝構造函數後,b.m_data=a.m_data,進行的是值拷貝,會將a.m_data的內容賦給b.m_data,b.m_data還是指向原來的內存區域,但是其內容改變。
有下面4個語句:
String a(“hello”);
String b(“world”);
String c = a; // 調用了拷貝構造函數,最好寫成 c(a);
c = b; // 調用了賦值函數
第3語句的風格較差,宜改寫成String c(a) 以區別於第4語句
下面是類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)
{
//檢查自賦值
if (this==&other)
{
return *this;
}
//釋放原有的內存資源
delete []m_data;
//分配新的內存資源,並複製內容
int length=strlen(other.m_data);
m_data=new char[length+1];
strcpy(m_data,other.m_data);
//返回本對象的引用
return *this;
}
類String拷貝構造函數與普通構造函數(參見9.4節)的區別是:在函數入口處無需與NULL進行比較,這是因爲“引用”不可能是NULL,而“指針”可以爲NULL。
類String的賦值函數比構造函數複雜得多,分四步實現:
(1) 第一步,檢查自賦值。
(2) 第二步,用delete釋放原有的內存資源。如果現在不釋放,以後就沒機會了,將造成內存泄露。
(3) 第三步,分配新的內存資源,並複製字符串。
(4) 第四步,返回本對象的引用,目的是爲了實現象 a = b = c 這樣的鏈式表達。注意不要將 return *this 錯寫成 return this.
Ref
高質量C++/C編程指南
http://man.chinaunix.net/develop/c&c++/c/c.htm#_Toc520634051