Effective C 讀書筆記(24)

2021-06-04 17:38:05 字數 3955 閱讀 2115

條款36:絕不重新定義繼承而來的non-virtual函式

never redefine an inherited non-virtualfunction

有如下**:

class b ;

class d: public b ;

雖然我們對b,d或mf一無所知,但面對乙個型別為d的物件x:

d x;

b *pb =&x; // 獲得乙個指標指向x

pb->mf(); // 經由該指標呼叫mf

d *pd =&x; // 獲得乙個指標指向x

pd->mf(); // 經由該指標呼叫mf

兩種情況中,pb和pd都呼叫了物件x中的成員函式mf。因為同樣的函式和同樣的物件,它們的行為應該相同。是的,但是也可能不。更明確地說,如果 mf 是non-virtual而 d 定義了它自己的mf版本:

class d:public b ;

pb->mf();// calls b::mf

pd->mf(); // calls d::mf

造成這一兩面行為的原因是,non-virtual函式如b::mf 和d::mf都是staticallybound(靜態繫結)的。這就意味著因為 pb被宣告為pointer-to-b型別,所以通過pb呼叫的non-virtual函式永遠是b所定義的版本,即使pb指向乙個型別為b的派生類的物件,一如本例。

在另一方面,virtual函式是dynamically bound(動態繫結)的,所以它們不會發生這個問題。如果 mf是乙個 virtual函式,無論通過pb還是pd呼叫mf都將導致d::mf的呼叫,因為 pb 和 pd真正指向的都是乙個型別為d的物件。

如果你在編寫class d且重定義了乙個從class b繼承到的non-virtual函式mf,d 的物件將很可能表現出不一致的行為。更明確地說,當mf被呼叫時,任何乙個d物件的行為既可能像b也可能像d,而且決定因素與物件本身無關,但是和指向它的指標宣告型別有關。引用也會展現和指標一樣難以理解的行為。

再從理論上分析,public inheritance意味著 is-a,在類中宣告乙個non-virtual函式是為這個類建立起乙個不變性,凌駕於特異性。因此:

l   每一件適用於b物件的事情也適用於d物件,因為每乙個d物件都 is-a(是乙個)d物件;

l   b的派生類一定會繼承mf的介面和實現,因為mf是b的non-virtual函式。

如果你要重新定義繼承而來的non-virtual函式,則原本基類中的函式可以定義為virtual以求得不同的實現;而既然你是以public方式繼承了基類,且那個函式不是virtual,就說明那個「不變性凌駕於特異性」的性質,即派生類可以直接使用基類中的那個non-virtual函式,且這樣做是符合需求的設計。

我們以前已經解釋了為什麼多型基類中的析構函式應該是virtual。如果你違反了那個準則(如在乙個多型基類中宣告乙個non-virtual析構函式),你也同時違反了本條款,因為派生類絕不應該重新定義乙個繼承而來的non-virtual函式(此處指基類的析構函式)。甚至對於沒有宣告析構函式的派生類,這也是成立的,因為如果你沒有定義你自己的析構函式,編譯器就會為你生成乙個。

條款37:絕不重新定義繼承而來的預設引數值

never redefine a function』s inheriteddefault parameter value

因為重新定義乙個繼承而來的non-virtual函式永遠是錯誤的,所以我們將本條款的討論限制在「繼承乙個帶有預設引數值的virtual函式」。

本條款成立的理由直接而明確:virtual函式是動態繫結(又名前期繫結)的,而預設引數值是靜態繫結(又名後期繫結)的。我們來回顧一下

物件的所謂靜態型別,就是它在程式文字中被宣告時所採用的型別。考慮這個類繼承體系:

class shape ;

// 所有形狀都必須提供乙個函式,用來繪製

virtual void draw(shapecolor color =red) const = 0;

...};

class rectangle:public shape ;

classcircle: public shape ;

現在考慮這些指標:

shape *ps;

shape *pc = new circle;

shape *pr = new rectangle;

在本例中,ps,pc 和 pr全被宣告為 pointer-to-shape型別,所以它們全都以此作為它們的靜態型別,無論它們真正指向什麼。

物件的所謂動態型別,是指目前所指物件的型別。也就是說,動態型別可以表現出乙個物件將會有什麼行為。在上面的例子中,pc的動態型別是 circle*,pr的動態型別是rectangle*。至於ps,它沒有乙個實際的動態型別,因為它尚未指向任何物件。動態型別,就像它的名字所暗示的,能在程式執行過程中改變(通常是經由賦值動作):

ps = pc;// ps的動態型別如今是circle*

ps = pr; // ps的動態型別如今是rectangle*

virtual函式系動態繫結而來,意味著當呼叫乙個virtual函式時,究竟呼叫哪乙份函式實現**,取決於發出呼叫的那個物件的動態型別:

pc->draw(shape::red);// calls circle::draw(shape::red)

pr->draw(shape::red); // calls rectangle::draw(shape::red)

當考慮帶有預設引數值的virtual函式時,因為virtual函式是動態繫結的,但預設引數是靜態繫結的。這就意味著你可能在呼叫乙個定義於派生類內的virtual函式的同時(動態),卻使用了基類為它指定的預設引數值(動態)。

pr->draw();// calls rectangle::draw(shape::red)!

此例中,pr的動態型別是rectangle*,rectangle的virtual函式被呼叫,在 rectangle::draw中預設引數值是green。然而pr的靜態型別是 shape*,這個函式呼叫的預設引數值是從shape中取得的,而不是rectangle!結果就是乙個呼叫由shape和 rectangle兩個類中的draw宣告式混合組成。

如果是引用,問題依然會存在。重點在於draw是乙個virtual函式,而它的乙個預設引數值在派生類中被重定義。

c++堅持這種乖張的方式來運作的原因是為了執行時效率。如果預設引數值是動態繫結的,編譯器就必須提供一種方法在執行時確定適當的virtual函式引數預設值,這比目前在編譯期確定它們的機制更慢而且更複雜。最終的決定偏向了速度和實現簡單這一邊,而造成的結果就是享受高效執行的樂趣。

如果試著遵循本規則,為基類和派生類的使用者提供同樣的預設引數值,又會造成**重複。更糟的是,**重複帶來相依性:如果shape 中的預設引數值發生變化,所有重複給定預設引數值的派生類必須同時變化。

當你要乙個virtual函式按照你希望的方式執行有困難的時候,考慮可選的替代設計是很明智的。這裡我們選擇nvi(non-virtual inte***ce idiom)手法:令基類內的乙個public non-virtual函式呼叫private virtual函式,後者可被派生類重新定義。這裡我們用non-virtual函式指定預設引數,而private virtual函式做實際的工作:

classshape ;

void draw(shapecolor color = red) const// now non-virtual

// call virtual

...

private:

virtual void dodraw(shapecolor color) const =0; // 真正工作在此處完成

}; classrectangle: public shape ;

因為non-virtual函式絕不應該被派生類覆寫,這個設計很清楚地使得draw的color預設引數值總是red。

Effective C 讀書筆記 2

讓自己習慣c 條款1 視c 為乙個語言聯邦 c 可以看作是四種次語言組成的 c 包括區塊 語句 預處理器 內建資料型別 陣列 指標等 object oriented c 主要表現c 的面對物件的性質,包括類 封裝 繼承 多型性 virtual函式等 template c 為c 泛型程式設計部分 st...

《effective C 》讀書筆記

1,c 關鍵字explicit c 中,乙個引數的 建構函式 或者除了第乙個引數外其餘引數都有預設值的多參建構函式 承擔了兩個角色。1 是個 構造器,2 是個預設且隱含的型別轉換操作符 所以,有時候在我們寫下如 aaa 這樣的 且恰好 的型別正好是aaa單引數構造器的引數型別,這時候 編譯器就自動呼...

Effective C 讀書筆記

一 讓自己習慣c 1 條款01 視c 為聯邦語言 c 的組成可分為四部分 1.c c 仍然以c語言為基礎。區塊 語句 預處理 內建資料型別 陣列 指標等都來自c。2.object oriented c c with classes所訴說的 classes 包括構造和析構 封裝 繼承 多型 virtu...