C++里面設計一個含指針的類比設計一個不含指針的類要復雜很多,因為需要考慮Big Three,也就是三個特殊函數: 拷貝構造函數(copy constructor), 拷貝賦值函數(copy assignment Operator) 析構函數(destructor)
為什么不含指針的類不需要考慮Big Three呢?因為C++編譯器會生成缺省拷貝構造函數和缺省拷貝賦值函數,這些函數采用memberwise copy,也就是淺拷貝。如果類不含指針的話,這也就夠用了。編譯器也會生成缺省的析構函數,但該析構函數不做任何操作。如果類不含指針,當對象生命期結束時析構也不用做什么事情,所以也沒問題。
但是類里面含有指針的話,C++編譯器生成的缺省拷貝構造函數和缺省拷貝賦值函數會采用淺拷貝將一個object的成員一項一項的拷貝給另一個object,這樣會導致同一個類的兩個object里面的指針指向同一個地址。這樣,如果object1的指針值賦值給了object2,那object2的指針原來指向的內存就沒人管了,造成內存泄漏。那如果object2的指針之前是NULL行不行呢?也不行。因為如果object 1修改了這個指針的內容,或者刪除了這個指針,object 2就躺著中槍了。
所以,類里面有指針的話,我們要自己設計Big Three。其中的 拷貝構造函數和拷貝賦值函數會生成新的指針,然后把指針指向的內容拷貝過來。這就是深拷貝。 以class string為例,代碼如下: class String { public: String(const char* cstr = 0); //構造函數 String(const String& str); //拷貝構造函數 String& operator=(const String& str); //拷貝賦值函數 ~String(); //析構函數 char* get_c_str() const {return m_data;} PRivate: char* m_data; }
先討論拷貝構造函數(copy ctor),代碼例子如下:
inline String::String(const String& str) { m_data = new char[ strlen(str.m_data)+1]; strcpy(m_data, str.m_data); }
注意,拷貝構造函數里m_data是新創建的指針,它指向的內容是拷貝過來的。
拷貝構造函數的用法如下: String s1(“hello”); String s2(s1); // line 1 String s2=s1; // line 2 上面的line 1和line 2都會調用拷貝構造函數將s1的字符串賦給s2。注意s2的m_data跟s1的m_data的值不同,但指向的內容是一樣的。
下面來談談拷貝賦值函數(copy assignment operator),代碼例子如下: inline String& String::operator=(const String& str) { if (this==&str) return *this; delete[] m_data; m_data=new char[ strlen(str.m_data)+1]; strcpy(m_data, str.m_data); return *this; }
這里有很多地方需要注意: 1) 拷貝賦值函數必須要檢查this指針是否已經是準備拷貝的對象里面的指針,換句話說就是不能把obj1賦值給obj1自己。因為如果不檢查的話,接下來的delete[]就把這個指針指向的內存給釋放了,這個指針就成了野指針。 再細想一下,為什么前面的拷貝賦值函數不需要檢查this指針呢?這是因為用拷貝賦值函數的時候,對象還沒有創建,所以this指針為空。
2) 為什么要delete[] m_data呢? 因為這里s2已經存在,它的m_data已經指向一段內存,里面已經有東西。直接把s1的m_data指向的字符串的內容拷貝給s2的m_data指向的內存是危險的,一是后者長度很可能不一樣,再一個可能會覆蓋了什么重要東西。所以我們要先把m_data指向的內容清除掉, 再重新創建m_data指針。
3) 還有一個問題,為什么要用delete[]而不是delete呢?其實這里是可以用delete的,因為char是基本數據類型,不涉及到析構函數。delete[]和delete都是把m_data指向的那個字符串的內容給釋放了,效果是一樣的。但是為了規范起見,new[]應該和delete[]對應。
拷貝賦值函數的用法如下: String s1(“hello”); String s2(s1); s2=s1; //line 3 這里s2會拷貝s1的字符串內容,注意s2和s1的m_data指向不同的地址。這里要特別注意的是: s2=s1 和上面的line2 String s2=s1; 不一樣,后者是調用的拷貝構造函數。在line3里面,s2這個object已經創建,里面的m_data指向的字符串已經有內容,而在line2里面,s2還沒有被創建。
下面來談談析構函數(dtor)。析構函數在以下三個地方會被用到。 1.object生命周期結束,被銷毀時; 2.delete指向object的指針,或delete指向object的基類類型指針并且基類析構函數是虛函數時; 3.object 1是object 2的成員,object 2的析構函數被調用時,object 1的析構函數也被調用。
String類的析構函數的例子如下: inline String::~String() { delete[] m_data; } 這里析構函數把m_data指向的字符串的內存給釋放了。注意這里因為char是基本類型,所以用delete m_data其實是可以的。但是為了規范,new[]和delete[]必須要對應。另外,delete[]或者delete怎么知道要釋放多少內存呢?因為new[]的時候編譯器已經知道長度了。
下面談談new/delete和內存以及構造函數和析構函數的關系: new: 先分配內存(這里會調用malloc),再調用ctor delete: 先調用dtor,再釋放內存(這里會調用free) 要注意的是,new[]一定要搭配delete[]。這里需要注意的是,對于C++的基本數據類型比如int,char的數組,因為不涉及析構操作(內部無指針),delete和delete[]效果是一樣的,都是釋放內存。但是對于一個class的object數組,delete和delete[]效果就不一樣了,舉例如下: String *p=new String[3];
delete[] p; 會調用String的析構函數三次,分別析構p[0],p[1],p[2],然后釋放掉p[]的所有內存。
delete p; 只會調用String的析構函數一次,析構p[0],然后釋放掉p[]的所有內存。那么這里問題就來了,p[1],p[2]都清掉了,p[1],p[2]的m_data所指向的內存也就沒人管了,造成2處內存泄漏。
關于delete p或delete[] p還要注意的一點是它只釋放指針指向的內存,至于指針的值它不會置為NULL。所以有時候為了安全起見,還要把p設置為NULL。
另外,關于析構函數還需要注意一點,Base class的析構函數一定要是虛函數。在Effective C++的條款14指出,當由基類指針刪除一個派生類對象是,如果基類的析構函數非虛,結果為未定義。舉例來說,假如Base的析構函數非虛的話: Base *p = new Derived(); // 此處會先調用基類的構造函數,然后調用子類的構造函數 delete p; //注意,此處只會調用基類的析構函數。
新聞熱點
疑難解答
圖片精選