構造函數、析構函數、賦值函數是每個類最基本的的函數。每個類只有一個析構函數和一個賦值函數。但是有很多構造函數(一個為復制構造函數,其他為普通構造函數。對於一個類A,如果不編寫上述四個函數,c++編譯器將自動為A產生四個默認的函數,即:
- A(void) //默認無參數構造函數
- A(const A &a) //默認復制構造函數
- ~A(void); //默認的析構函數
- A & operator = (const A &a); //默認的賦值函數
既然能自動生成函數,為什么還需要自定義?原因之一是“默認的復制構造函數”和"默認的賦值函數“均采用”位拷貝“而非”值拷貝“
位拷貝 v.s. 值拷貝
為便於說明,以自定義String類為例,先定義類,而不去實現。
#include <iostream> using namespace std; class String { public: String(void); String(const String &other); ~String(void); String & operator =(const String &other); private:
char *m_data;
int val; };
位拷貝拷貝的是地址,而值拷貝拷貝的是內容。
如果定義兩個String對象a, b。當利用位拷貝時,a=b,其中的a.val=b.val;但是a.m_data=b.m_data就錯了:a.m_data和b.m_data指向同一個區域。這樣出現問題:
- a.m_data原來的內存區域未釋放,造成內存泄露
- a.m_data和b.m_data指向同一塊區域,任何一方改變,會影響到另一方
- 當對象釋放時,b.m_data會釋放掉兩次
因此
當類中還有指針變量時,復制構造函數和賦值函數就隱含了錯誤。此時需要自己定義。
結論
- 有一種特別常見的情況需要自己定義復制控制函數:類具有指針哈函數。
- 賦值操作符和復制構造函數可以看成一個單元,當需要其中一個時,我們幾乎也肯定需要另一個
- 三法則:如果類需要析構函數,則它也需要賦值操作符和復制構造函數
注意
- 如果沒定義復制構造函數(別的不管),編譯器會自動生成默認復制構造函數
- 如果定義了其他構造函數(包括復制構造函數),編譯器絕不會生成默認構造函數
- 即使自己寫了析構函數,編譯器也會自動生成默認析構函數
因此此時如果寫String s是錯誤的,因為定義了其他構造函數,就不會自動生成無參默認構造函數。
復制構造函數 v.s. 賦值函數
#include <iostream> #include <cstring> using namespace std; class String { public: String(const char *str); String(const String &other); String & operator=(const String &other); ~String(void); private: char *m_data; }; String::String(const char *str) { cout << "自定義構造函數" << endl; 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(const String &other) { cout << "自定義拷貝構造函數" << endl; int length = strlen(other.m_data); m_data = new char[length + 1]; strcpy(m_data, other.m_data); } String & String::operator=(const String &other) { cout << "自定義賦值函數" << endl; if (this == &other) { return *this; } else { delete [] m_data; int length = strlen(other.m_data); m_data = new char[length + 1]; strcpy(m_data, other.m_data); return *this; } } String::~String(void) { cout << "自定義析構函數" << endl; delete [] m_data; } int main() { cout << "a(\"abc\")" << endl; String a("abc"); cout << "b(\"cde\")" << endl; String b("cde"); cout << " d = a" << endl; String d = a; cout << "c(b)" << endl; String c(b); cout << "c = a" << endl; c = a; cout << endl; }
執行結果
說明幾點
1. 賦值函數中,上來比較 this == &other 是很必要的,因為防止自復制,這是很危險的,因為下面有delete []m_data,如果提前把m_data給釋放了,指針已成野指針,再賦值就錯了
2. 賦值函數中,接着要釋放掉m_data,否則就沒機會了(下邊又有新指向了)
3. 拷貝構造函數是對象被創建時調用,賦值函數只能被已經存在了的對象調用
注意:String a("hello"); String b("world"); 調用自定義構造函數
String c=a;調用拷貝構造函數,因為c一開始不存在,最好寫成String c(a);
C++賦值運算符函數
為類添加賦值運算符函數:
類型定義
class CMyString { public: CMyString(char *pData = NULL); CMyString(const CMyString &str); ~CMyString(void); CMyString &operator=(const CMyString &); private: char *m_pData; };
要點:
1、返回值類型為該類型的引用,並在函數結束前返回實例自身的引用(即 *this);
2、是否把傳入的參數聲明為常量引用【const CmyString &str】;
3、是否釋放自身已有內存,否則會造成“內存泄漏”;
4、是否判斷參數與當前示例是指向的同一個對象;
解法:
/* 適用於初級C++程序員的解法 */ CMyString &CMyString::operator=(const CMyString &str) { //首先檢測兩個指針是否指向同一個對象 if (this == &str) return *this; //釋放原內存 delete []m_pData; m_pData = NULL; //重新申請內存 m_pData = new char[strlen(str.m_pData) + 1]; strcpy(m_pData,str.m_pData); //謹記:返回*this return *this; } /* 適用於高級C++程序員的解法 */ CMyString &CMyString::operator=(const CMyString &str) { if (this != &str) //確保不指向同一個實例 { CMyString strTemp(str); char *pTemp = strTemp.m_pData; //指針指向需要更換的對象 //strTemp.m_pData指向原來的對象,確保內存不足時可以找到原來對象的值 strTemp.m_pData = m_pData; m_pData = pTemp; //更換原對象的值 } //自動調用strTemp的析構函數,銷毀strTemp對象並回收pTemp的內存 return *this; }