引:一道經常見到的面試題 ,已知類String的原型為:
class String
{
public:
String(const char *str = NULL);// 普通構造函數
String(const String &other); // 復制構造函數
~ String(void); // 析構函數
String & operate =(const String &other);// 重載賦值操作符
private:
char *m_data;
};
請實現上述String的四個函數。
這道題涉及到了類型的復制構造函數,賦值操作符和析構函數的實現,其中有許多要注意的地方。參考c++ primer第13章
1. 復制構造函數
它是一種特殊的構造函數,單個形參,該形參為對該類型的引用(常為const),當定義一個新對象並用銅類型的對象進行初始化時,將顯示的調用復制構造函數,當把該類型的對象傳遞給函數或從函數返回該類型的對象時,將隱式的調用復制構造函數。
a.為什么形參必須為引用?考慮如下代碼
class A { private: int value; public: A(int n){value=n;} A(A other){value=other.value;} void functionA(){}; ~A(){} }; int main(void) { A a = 10; A b = a; b.functionA(); }
通常,這段代碼會顯示編譯錯誤的,假設能編譯執行,那么由於A(A other)是值傳遞,在執行A b = a 時,會調用A(A other)並把a當作形參復制到實參(隱式調用A(A other)),即出現了在復制構造函數中調用構造函數,形成無條件遞歸。所以C++標准不允許復制構造函數為值傳遞。正確形式應為:A(const A& other)。
b.何時需要自定義復制構造函數?
如果我們沒有定義復制構造函數,編譯器會為我們合成一個(即便定義了其他構造函數),默認行為為逐個成員初始化為原對象的副本。
當類中有指針,或有成員在構造時需要分配其他資源,通常需要定義復制構造函數。這其中有涉及到對象的深拷貝和淺拷貝,如果一個類擁有資源(A如char *str 指向一個字符數組),當這個類的對象發生復制過程時,資源再次分配(B:str=new char[length+1]; if(str!=NULL);strcpy(str,A.str);),這個過程就是深拷貝,反之,沒有再次分配資源(B:str=A.str),就是淺拷貝;
類禁止復制時,需要定義復制構造函數(聲明為private)。
通過以上,則String的復制構造函數可以定義如下:
String::String(const String& other) { int length = strlen(other.m_data); m_data = new char[length+1]; //注意'\0' strcpy(m_data, other.m_data); }
2.重載賦值操作符
賦值操作符以this(隱藏)和同類型對象的const引用作為形參。返回對同類型的引用。通常一個類定義復制構造函數的同時需要定義賦值操作符。
a.在賦值之前,需要釋放當前對象的資源,否則會形成內存泄露;又因為如此,在這之前需要判斷傳入的參數和當前對象是否是同一個實例,否則delete時會造成嚴重錯誤。
String& String::operator=(const String& other) { if(this == &other) { return *this; } delete[] m_data; int length = strlen(other.m_data); m_data = new char[lenght+1]; if(m_data != NULL) { strcpy(m_data,other.m_data); } return *this; } //可能有更好更安全的實現,以后再說
3.析構函數
a.銷對象時會調用析構函數。對於動態分配的對象,只有指向該對象的指針被刪除時才撤銷。析構函數通常釋放在構造函數或在對象生命周期內獲取的資源;
b.默認析構函數(編譯器總會生成),它按成員在類中聲明的次序的逆序撤銷成員(非static)。但默認析構函數不刪除指針指向的對象;
c.析構函數沒有返回值,沒有形參,所以不能重載它。
String::~String()
{
delete [] mdata;
}
后記:通常這三個復制控制成員中我們定義一個,則它也需要定義另外兩個 ,即所謂的“rule of three”。