C++中的一個更為重要的思想是使用者自訂類型可以很容易地當作內建類型使用。通過定義新類型,使用者可以為了他們自己的目的來定製語言。這種強大的工具如果被錯誤的使用,便會十分危險。實際上,設計類庫和設計程式設計語言是相似的,而且應該給予高度的重視。
字串類
1 class String {
2 public:
3 String(char* p){
4 sz = strlen(p);
5 data = new char[sz + 1];
6 strcpy(data, p);
7 }
8 ~String(){delete[] data;}
9 operator char*() {return data;}
10 private:
11 int sz;
12 char* data;
13 };
差強人意的設計,沒有考慮到異常情況的發生,例如記憶體耗盡。
記憶體耗盡
記憶體耗盡什麼情況下發生?
使用者請求了一個很大的String,而又沒有足夠的記憶體空間,就會發生記憶體耗盡的情況。
這種情況下會發生什麼事情?
設計沒有考慮,無法預測,但問題是發生在new運算式。
new運算式失敗會發生什嗎?
可能會發生3件事情中的一件:庫拋出異常,或者整個程式伴隨著一個適當的診斷資訊退出,或者new運算式返回0。
拋出異常和new運算式返回0,哪種方法好一些?
判斷data是否等於0的做法,似乎可行,但是類內部使用data的時候都要做判斷;拋出異常只需在new的時候處理一下,使用者也可以通過try捕捉,程式寫法上比較簡單。
1 class String {
2 public:
3 String(char* p){
4 sz = strlen(p);
5 data = new char[sz + 1];
6 if(data == 0)
7 throw std::bad_alloc();
8 else
9 strcpy(data, p);
10 }
11 ~String(){delete[] data;}
12 operator char*()
13 {
14 return data;
15 }
16 private:
17 int sz;
18 char* data;
19 };
複製引發的記憶體問題
String類定義中沒有複製建構函式和賦值操作符,這樣,編譯器代表程式員建立他們,並用對類成員的相應複製操作遞迴地定義它們,因此,複製一個String就相當於複製String的sz和data的成員的值。這就導致了,複製完後,原來的data成員和副本的data成員將指向相同的記憶體,所以,兩個String被釋放時,該記憶體會被釋放兩次。
最簡單的解決辦法是通過私人化複製建構函式和賦值操作符來規定不能複製String,賦值操作符不能是虛函數。
複製建構函式和賦值操作符之間的主要區別在於:賦值操作符複製新值進來前必須刪除就值。複製的部分用assign函數來完成。
1 class String {
2 public:
3 String(char* p){
4 assign(p, strlen(p));
5 }
6 String(const String& s){
7 assign(s.data, s.sz);
8 }
9 ~String(){delete[] data;}
10 operator char*()
11 {
12 return data;
13 }
14 String& operator=(const String& s){
15 //不能先刪除資料然後調用assign,因為把一個String賦給它自身肯定會失敗
16 if(this != &s){
17 delete[] data;
18 assign(s.data, s.sz);
19 }
20 return *this;
21 }
22 private:
23 int sz;
24 char* data;
25 void assign(const char* s, unsigned len)
26 {
27 data = new char[len + 1];
28 if(data == 0)
29 throw std::bad_alloc();
30 sz = len;
31 strcpy(data, s);
32 }
33 };
針對使用者,適當的隱藏實現是類設計者一個重要的職責,那隱藏實現的作用是什麼呢?
隱藏實現
隱藏實現的作用:我們通常把資料隱藏視作是保護類設計者的一種措施,它給我們帶來了一定的靈活性,方便以後根據需要修改實現,而且適當的隱藏實現也是協助防止使用者出錯的重要方法。
operator char*()所暴露的問題:
- 通過該運算子取得的指標,使用者可能會修改data中的內容;
- 釋放String時,它所佔用的記憶體也會被釋放,這樣任何指向String的指標都會失效;
- 通過該運算子釋放和重新分配目標Stirng使用的記憶體來將一個String的賦值,可能會導致任何指向String內部的指標失效。
為瞭解決這三個問題,作者想以
operator const char*() const{
return data;
}
來解決第1個問題,而無法解決第3個問題,於是放棄這種做法。作者反省到記憶體管理的工作應該交給使用者,更明智的做法是讓使用者提供將data複製進去的空間,用
1 void make_cstring(char* p, int len) const{
2 if(sz <= len)
3 strcpy(p, data);
4 else
5 throw("Not enough memory supplied");
6 }
來實現。
預設建構函式
對於
String s;
String s_arr[20];
的處理,添加預設建構函式
1 String(): data(new char[1]){
2 sz = 0;
3 *data = '\0';
4 }