C++類設計者的核查表(一)

覈查表不是任務清單。它的用途是幫助你回憶起可能會忘掉的事情,比如飛行員覈查表需要覈查飛行過程中需要注意的事情,防止發生一些不必要的意外。下面是一些C++類的設計覈查,這些問題並沒有一個確切的答案,但是它會提醒你思考它們,並確定你做的事情是有意識的決定,而不是偶然事件。

  • 你的類需要一個構造函數嗎?
    有些類比較簡單,他的結構就是它的接口,因此不需要構造函數。但是如果是一些比較複雜的類,它們需要構造函數來隱藏其內部的工作方式。

  • 你的數據成員是私有的嗎?
    通常使用公有的數據成員不是什麼好事,因爲類的設計者無法控制何時訪問這些成員,例如下面一個支持可變長矢量的類:

template<typename T>
class Vector
{
public:
	int length;
};

如果類的設計者將矢量的長度設置爲一個成員變量,那麼設計者就必須保證這個變量在任何時候都可以顯示矢量的實際長度,因爲不知道什麼時候會訪問這個信息。另一方面,如果函數在類中是這樣實現的,比如:

template<typename T>class Vector {
public:
	int length() const;
};

這樣除非用戶調用,否則vector根本不必計算長度。這個思想就很重要了。
另外,使用函數而不是變量,在還允許讀取訪問的時候能夠容易的阻止寫入訪問。vector的第一個版本根本沒有阻止用戶改變長度的措施。原則是length可以是一個const int,但是如果創建後還要改變長度,就不能這樣做了。但是我們可以通過引用來只允許用戶進行讀取訪問:

template<typename T>class Vector {
public:
	const int& length;	//每個構造函數都將length綁定到true_length
	// ...
private:
	int true_length;
};

這樣做確實可以防止以後出錯,但還是不如一開始直接用函數實現length方便。這樣用戶不可以改變length的值,所以會返回一個實際值。

但是如果類的設計者希望用戶可以改變length的值,那將其設置爲public也不是個好辦法,和複製vector的值一樣,改變長度大致上也需要手動分配和回收內存。如果length是一個用戶直接設置的變量,就無法迅速檢測到用戶的變化,所以對這種改變的檢測總是滯後的,很可能是在操作vector時才檢測到length的變化。但是使用成員函數改變length,每次改變時都會調用成員函數,這樣每次用戶改變length時設計者都可以察覺到。

如果有兩個操作length的函數,是不是應該使用set_length和get_length來對長度進行不同的操作,而且其返回值是void還是一個值?或者返回一個bool變量,是否操作成功?還是返回一個值,變化前的值還是變化後的值?所以要規範自己的設計,對代碼可讀性有很大的影響。

  • 你的類需要一個無參的構造函數嗎?

如果一個類已經有了構造函數,而你想讓你的類不必顯式的初始化它們,則必須顯式的寫一個無參的構造函數——例如:

class PointDemo
{
public:
	PointDemo(int p, int q) :x(p), y(q) {};
	//...
private:
	int x, y;
};

這裏我們定義了一個有構造函數的類。除非這個類有一個不需要參數的構造函數,否則下面的語句就是非法的:

PointDemo p;

這裏沒有指出怎麼初始化p。此外,如果一個類需要一個顯示構造函數,比如上面的類,則試圖創建該類的對象數組是非法的:

PointDemo p[100]

即使你想把所有的對象都實例化,也要考慮所付出的代價。

  • 是不是每個構造函數都需要初始化所有的數據成員?

構造函數的用途就是用一種明確定義的狀態來設置對象。對象的狀態由對象的數據成員進行反映。因此每個構造函數都需要負責爲所有的數據成員設置經過定義的值。如果構造函數沒有做到這一點,就可能導致錯誤。
當然,這也不是完全正確的,比如類有一些數據成員,它們只在它們對象存在的一段時間有意義。

  • 類需要析構函數嗎?
    不是所有有構造函數的類都需要析構函數。例如表示複數的類即使有析構函數,但是也不需要。如果深入考慮一個類需要做什麼,那麼考慮其是否需要析構函數就很重要了。應該問問該類是否分配了資源,而這些資源又不會由成員函數自動釋放,這就足夠了。尤其是那些構造函數中包含了new的類,析構函數中還需要加一個delete來釋放內存。

  • 類需要一個虛析構函數嗎?
    虛析構函數在繼承時有很重要的作用,因此,絕對不會當作基類的類是不需要虛析構函數的。虛析構函數會在釋放資源時一步步的釋放,從而實現遞歸釋放資源,如果不是虛析構函數則會調用錯誤的虛構函數,這個在我以前的文章中有講到。並且,虛析構函數通常是空的。

  • 你的類需要複製構造函數嗎?
    很多時候都是不,但是有時候是需要。關鍵在於複製時是否相當於複製其數據成員和基類對象,如果並不相當,則需要複製構造函數。
    如果你的類在構造函數內分配資源,則可能需要一個顯式的複製構造函數來管理資源。有析構函數(除了空的虛析構函數外)的類通常是用析構函數來釋放構造函數分配的資源,這通常也說明需要一個複製構造函數,例如:

class String
{
public:
	String();
	String(const char* s);
private:
	char *data;
};

很明顯,它需要一個析構函數,因爲它的數據成員指向了必須由對應的對象釋放的被動態分配的內存。同理,它還需要一個顯式的複製構造函數:沒有的話,複製string對象就會複製它的data成員的形式隱式的定義。複製完後,兩個對象的data成員將指向同樣的內存;當這兩個對象被銷燬時,這個內存會被釋放兩次。在拷貝構造函數時需要重新分配內存,而且在析構函數中需要加入判斷指針是否爲空的判斷條件,使析構函數正確的釋放分配的資源。
當然,如果不希望用戶能夠複製類的對象,就聲明構造函數(可能還要賦值操作符)爲私有的:

class String
{
public:
	//...
private:
	String(const String&);
	String &operator=(const String&);
};

如果沒有其他的成員使用這些成員函數,如上聲明就夠了。沒有必要定義它們,因爲沒人會調用它們,你不能,用戶也不能。

這些便是我對C++類設計的一些個人見解,希望對大家有所幫助。如果有不對的地方歡迎大家批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章