條款18:讓介面容易被正確使用,不易被誤用
開發乙個「容易被正確使用,不易被誤用」的介面,首先必須考慮使用者會做出什麼樣的錯誤。以下為例:
class date
;
乍見之下這個介面通情達理,但是至少容易犯兩個錯誤。第一,他們可能以錯誤的次序傳遞引數;第二, 他們可能傳遞乙個無效的引數。許多客戶端錯誤可以因為匯入新的型別而獲得預防:
class month};
class day};
class year
};class date
};
預防客戶錯誤的另乙個辦法是限制型別內什麼事可做,什麼事不可做,常見的限制是加上
const。如下例:
if(a*b = c) …//其實原意是做一次比較動作
如果過載的乘法操作返回的是const,編譯器就會識別出這種賦值運算不恰當了。
另外,「除非有好理由,否則應該盡量令你的
types
的行為與內建
types
一致」。比如不要在乘法運算中做加法運算。很少有其他性質比「一致性」更能導致介面容易被正確使用。比如長度,不要有的型別用size表示,有的型別用length表示。雖然有些ide外掛程式能夠自動去尋找相應的方法名,但「不一致性對開發人員造成的心理和精神上的摩擦與爭執,沒有任何乙個ide可以完全抹除」。
任何介面如果要求客戶必須記得做某件事,就有著「不正確使用」的傾向,因為客戶可能忘記做某件事,最容易造成資源洩露。在前面資源管理中提到的智慧型指標,讓專門的資源管理來掌握資源的**:
auto_ptrcreateinvestment();
shared_ptrcreateinvestment();shared_ptr有乙個特別好的性質:它會自動使用它的「每個指標專屬的刪除器」,因而消除另乙個潛在的客戶錯誤:所謂的「cross-dllproblem」。這個問題發生於在不同的dll中出生(new)和刪除(delete)的情況(物件生命週期橫跨兩個dll,但在第二個dll中結束生命的時候卻希望呼叫的是第乙個dll的析構函式),自定義刪除器則會在刪除時仍然呼叫誕生時所在的那個dll的析構函式。智慧型指標比原始指標大且慢,而且使用輔助動態記憶體。在許多應用程式中這些額外的執行成本並不高,然而其降低客戶錯誤的成效確很明顯。
請記住:
1、 好的介面容易被正確使用,不易被誤用
2、 介面的一致性,以及與內建型別的行為相容
3、 「阻止誤用」的辦法包括建立新型別、限制型別上的操作、束縛物件值、以及消除客戶的資源管理責任
4、 多使用資源管理類如智慧型指標shared_ptr、auto_ptr等進行資源管理
條款19:設計class猶如設計type
新type的物件應該如何被建立和銷毀?這影響到class的建構函式、析構函式以及記憶體分配和釋放函式的設計。
物件的初始化和物件的賦值該有什麼樣的差別?決定拷貝建構函式和賦值操作符的行為,以及之間的差異
新type的物件如果被passed by value,意味著什麼?拷貝建構函式的實現
什麼是新的type的「合法值」?對於class的成員變數而言,通常只有某些數值是有效的,所以通常要進行錯誤檢查、異常處理等
新的type需要配合某個繼承的類嗎?繼承某些classes必然受到那些classes的設計的束縛,特別是virtual和non-virtual的影響
新的type需要什麼樣的轉換?如果期待進行允許的型別t1物件隱式轉換為型別t2物件,就必須在classt1寫乙個型別轉換函式(operator t2)或class t2內寫乙個可被單一實參呼叫的建構函式。如果只允許explicit建構函式,就得寫出專門負責指向轉換的函式
什麼樣的操作符和函式對此type而言是合理的?決定class宣告哪些函式
什麼樣的標準函式應該駁回?那些正是你必須宣告為private的
誰該取用新type的成員?決定函式為public、private、protected或friends
什麼是新type的「未宣告介面」它對效率、異常安全性以及資源的運用提供何種保證
新type有多一般化?如果不僅是定義乙個新type,而是一整個types家族,就應該定義新的classtemplate
真的需要乙個新type嗎?
請記住:
class的設計就是type的設計,在定義乙個新的type之前,請確定你已經考慮過本條款中所有討論的主題。
條款20:寧以pass-by-reference-to-const替換pass-by-value
在預設的情況下,c++以by value的方式傳遞物件至函式,這些物件由copy建構函式產出,使得pass-by-value成為昂貴的操作,考慮以下繼承:
classperson;
classstudent:publicperson;
現在有乙個呼叫函式validatestudent,要呼叫乙個student實參並返回它是否有效?
boolvalidatestudent(students);
student plato;
boolplatoisok=validatestudent(plato);
person一次copy建構函式、student一次copy建構函式、四次stringcopy建構函式,以及他們的析構函式。改進方法:
bool validatestudent(const student& s);
這種方式高效:沒有任何建構函式和析構函式,也沒有任何新物件構建。此外,這種方式還可以避免物件的切割(slicing)。
如果窺視c++編譯器的底層,reference往往是以指標的方式實現的,因此對於內建型別passe-by-value更合理,這忠告同樣是適用於stl迭代器和函式物件。
請記住:
1、 盡量以passby reference to const替換pass by value。前者通常比較高效,並可避免切割的問題。
2、 以上規則並不適用於內建型別,以及stl的迭代器和函式物件。對它們而言,passby value更加適當。
條款21:必須返回物件時,別妄想返回其reference
在上條款中談到的pass-by-value一般都以pass-by-reference替代,但是容易存在過度使用的問題:只要看到是乙個非內建型別,就去使用引用傳值。
class rational
friend const rational& operator* (constrational& r1, const rational& r2)
};
函式建立物件有兩種方式:在stack空間或heap空間建立。首先如果定義乙個local變數,就在stack建立物件:
friend constrational& operator* (const rational& r1, const rational& r2)
返回local物件的引用是不可接受的,因為
local
在函式退出前已經被銷毀了。於是讓我們考慮在heap內構造乙個物件,並返回reference指向它:
friend constrational& operator* (const rational& r1, const rational& r2)
無法析構物件,導致資源洩露。
static物件位於全域性靜態區,它的生命週期與這個程式的生命週期是相同的,所以不用擔心它會像棧物件那樣很快消失掉,也不用擔心它會像堆物件那樣有資源洩露的危險。可以像這樣寫:
friend constrational& operator* (const rational& r1, const rational& r2)
這樣可以正常編譯通過,但是對於:
rational a, b, c,d;
if((a*b) == (c*d))
if條件恒為真,這就是靜態物件做的!因為返回值共享這個靜態物件。
乙個必須返回新物件的正確寫法是去掉引用,返回乙個新物件:
friend constrational operator* (const rational& r1, const rational& r2)
當然,你需要承受返回值的構造和析構成本,但是從長原來看是為了獲得正確行為而付出的乙個小小的代價。
請記住:
絕對不要返回pointer或reference指向乙個localstack物件,指向乙個heap-allocated物件也不是好方法,更不能指向乙個local static物件(陣列),需要複製物件的時候,就讓它去複製。
設計與宣告
預設情況下c 是以值傳遞的形式傳遞物件到函式的。除非特別指定,否則函式引數都是以實際實參的副本為初值。呼叫端所獲的也是函式返回值的乙個副本。這些副本是由物件的拷貝建構函式產生,這會使值傳遞非常費時。特別是在乙個結構複雜的類中,例如 class student public person當有以下函式 ...
設計與宣告
條款18 讓介面容易被正確使用,不易被誤用 條款19 設計class猶如設計type 條款20 寧以pass by value to const替換pass by value 預設情況下c 以by value方法給物件傳遞引數,函式引數都是物件的副本,這些副本是由物件的copy建構函式產出,這可能使...
二 設計與宣告
類的設計是一件非常複雜的事情,設計師在設計類的時候不僅僅要考慮實現的功能,又要兼顧擴充套件性 易用性等一些功能外的特性。在使用外部庫的時候,在不查閱文件的基礎上,我們常常煩惱於如何呼叫api,一些引數非常容易混淆。舉乙個例子我們在學生系統中要對一名學生進行輸入生日的操作。item19常常我們使用a ...