溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

關(guān)于C++面向?qū)ο笤O(shè)計(jì)的訪問性問題詳解

發(fā)布時(shí)間:2020-09-28 18:32:26 來源:腳本之家 閱讀:133 作者:taozj 欄目:編程語(yǔ)言

前言

最近在看Scott Meyers大神的《Effective C++》和《More Effective C++》,雖然這兩本書都是古董級(jí)的教參了(當(dāng)然針對(duì)C++11/C++14作者所更新的《Modern Effective C++》英文已經(jīng)發(fā)售了,不過還沒中文翻譯版本),但是現(xiàn)在看來仍然收益匪淺,而且隨著對(duì)這個(gè)復(fù)雜語(yǔ)言了解的深入和實(shí)踐項(xiàng)目經(jīng)驗(yàn)的增加,很多東西和作者產(chǎn)生了一種共鳴,以前種種疑惑突然有種撥云霧而見天日、豁然開朗的感覺,也難怪被列為合格C++程序員之必讀書目。其實(shí)C++確實(shí)是個(gè)可怕的語(yǔ)言,于是市面上針對(duì)這個(gè)語(yǔ)言的教參也是聆郎滿目層出不窮,當(dāng)然水平也是參差不齊,像上面所說的Meyers三部曲能夠歷久彌新,也凸顯了這些經(jīng)典教參的真正價(jià)值。

至于最近回歸C++本質(zhì),主要是覺得現(xiàn)在后臺(tái)開發(fā)的RPC、MQ、分布式系統(tǒng)雖然被稱的神乎其神的,但是作為成熟的組件絕大多數(shù)公司都可以是直接拿來主義,當(dāng)然也不可否認(rèn)其使用經(jīng)驗(yàn)的可貴,因?yàn)樽罱€上使用這些組件還是遇到或多或少不少問題的,以后可以少走些坑,然而這種東西也是可遇難求的;反而C++語(yǔ)言本身的使用占用了程序員絕大多數(shù)的工作內(nèi)容,從而直接影響到項(xiàng)目的質(zhì)量和后續(xù)的可維護(hù)性。在此,侯捷老師的 勿在浮沙筑高臺(tái) 仍如警世名言響徹在耳,一個(gè)合格的程序員其扎實(shí)的基本功是多么重要。

C++面向?qū)ο蟮臇|西太多了:public、protected、private訪問和繼承,virtual和多態(tài)、多繼承,外加const、缺省參數(shù)、名字查找等,光這些元素的排列組合就可以導(dǎo)出很多種情況,看似靈活多變,但不是每種情況都值得去嘗試的。

一、public繼承

public繼承意味著是”is-a”的關(guān)系,每個(gè)派生類型對(duì)象也是一個(gè)基類類型對(duì)象,基類支持的操作派生類都支持,只不過派生類比基類更具體化一些而已,否則的話應(yīng)該將派生類不支持的特性給踢出去,比如:

class Bird { ... };
class FlyingBird: public Bird {
public:
 virtual void fly(); ...
};
class Penguin: public Bird { ... };

所以,總體來說public繼承是相對(duì)比較嚴(yán)格的契約關(guān)系。當(dāng)然public繼承是一個(gè)比較籠統(tǒng)的概念,細(xì)分下來還包括接口繼承、實(shí)現(xiàn)繼承、接口和實(shí)現(xiàn)繼承。

如果基類聲明了一個(gè)pure virtual函數(shù),則其目的是讓派生類只繼承該函數(shù)接口;如果基類聲明了一個(gè)impure virtual函數(shù),就是讓派生類繼承該函數(shù)的接口和其缺省實(shí)現(xiàn);如果某個(gè)成員函數(shù)是non-virtual函數(shù),則意味著它不打算在派生類中有不同的行為,即派生類繼承該函數(shù)接口及一份強(qiáng)制性實(shí)現(xiàn)。

對(duì)于pure virtual函數(shù)的接口聲明,基本沒有什么意義,而non-virtual成員也顯而易見。不過對(duì)于impure virtual虛函數(shù),看似提供了缺省實(shí)現(xiàn)使用起來會(huì)比較方便,而且派生類可以覆蓋其實(shí)現(xiàn)也比較靈活,但是如果直接使用這種方式,那么如果基類產(chǎn)生了新的派生類,但是恰好派生類忘記對(duì)這個(gè)impure virtual函數(shù)進(jìn)行override,而其缺省實(shí)現(xiàn)又不滿足新派生類的行為,那么新派生類對(duì)象的調(diào)用將會(huì)引發(fā)問題。所以如果想繼承接口,同時(shí)又提供缺省實(shí)現(xiàn),那么比較好的方式是將這兩個(gè)功能進(jìn)行分離,用一個(gè)pure virtual函數(shù)提供接口,再用一個(gè)non-virtual protected函數(shù)提供缺省實(shí)現(xiàn),而讓派生類手動(dòng)確認(rèn)是否使用該默認(rèn)行為。

class Airplane {
public: virtual void fly(const Airport& dest) = 0;
protected: void defaultFly(const Airport& dest){ ... }
};

除了上面的方式處理impure virtual的缺省實(shí)現(xiàn),其實(shí)也可以將其轉(zhuǎn)換為:仍然使用pure virtual函數(shù)聲明接口,不同同時(shí)也提供其缺省定義,這樣派生類在override這個(gè)pure virtual接口的時(shí)候既可以完全重新定義fly的行為,也可以直接一條語(yǔ)句用基類名字直接調(diào)用基類的缺省實(shí)現(xiàn)(Airplane::fly),其好處是不用引入一個(gè)新的函數(shù)名字,缺點(diǎn)是缺省實(shí)現(xiàn)成了public的了。

說到此處,應(yīng)該對(duì)C++中接口繼承的行為得以了解了。

二、虛函數(shù)外的其他選擇

前面我們說到了《C++之virtual函數(shù)訪問性》中談及了NVI手法,算是對(duì)public virtual的一個(gè)強(qiáng)有力的替換工具,不過我們知道其本身也用到了虛函數(shù)。虛函數(shù)具有運(yùn)行時(shí)開銷,而且其實(shí)現(xiàn)也是編譯時(shí)間確定運(yùn)行時(shí)候選擇,在有些情況下其靈活性還是受限。

相比于虛函數(shù)依據(jù)派生類型進(jìn)行行為的定制化之外,Strategy策略模式顯得更為的靈活。通過在對(duì)象內(nèi)部保存函數(shù)指針(或者更泛化的boost::function函數(shù)對(duì)象),其行為可以依據(jù)具體對(duì)象差異化而非派生類型差異化,甚至通過Set接口其行為還可以在運(yùn)行時(shí)候進(jìn)行變更。雖然Meyers說明如果使用非成員函數(shù),默認(rèn)將不能訪問類的私有成員,否則就需要對(duì)封裝性進(jìn)行一定程度的妥協(xié)松懈,但是通過boost::function+boost::bind這個(gè)強(qiáng)有力的工具,使用繼承體系中的成員函數(shù)也是十分方便的。

此處本人感覺,雖然設(shè)計(jì)模式被奉為C++開發(fā)的經(jīng)典,但是隨著Modern C++在標(biāo)準(zhǔn)上引入更多的特性和功能,C++的開發(fā)將必定變的更加友好直觀,也不被過于墨守那古典23式了,畢竟絕大多數(shù)的設(shè)計(jì)模式都通過繼承來實(shí)現(xiàn)的,不可避免的增加了程序開發(fā)和維護(hù)的復(fù)雜度。

三、繼承體系來的其他問題

好了,輕松愉快的東西結(jié)束了,下面是C++史上的黑暗時(shí)刻了。

3.1 繼承而來名字的可見性

C++具有一套名字查找的規(guī)則,總體來說就是從局部到外圍,從派生類到基類,從內(nèi)層名字空間到全局名字空間的查找順序。

由于到此為止我們沒有說明函數(shù)重載的情況,所以你此時(shí)仍然安之若素:對(duì)于public non-virtual函數(shù)我們不去重寫,對(duì)于virtual函數(shù)我們可以override,這一切安好,但是一旦考慮到相同函數(shù)名的重載問題,C++有一套理論就會(huì)讓你暈乎了:C++防止在應(yīng)用程序庫(kù)或者應(yīng)用框架中建立的新的派生類被附帶從疏遠(yuǎn)的基類中繼承而來的重載函數(shù),所以在繼承的時(shí)候C++不會(huì)將基類的名字自動(dòng)導(dǎo)入到派生類中。

好了,這就說明,之前繼承而來的接口,其實(shí)也是在使用的時(shí)候在派生類作用域中沒有找到該符號(hào),而在基類中找到該符號(hào)后滿足調(diào)用的,而如果你在基類中定義了其某個(gè)重載版本(無(wú)論是virtual還是non-virtual)的時(shí)候,C++在名字查找的時(shí)候就在你的派生類作用域中找到該名字了,然后進(jìn)行類型檢查和重載,但是重載的版本只限于在派生類中出現(xiàn)的版本,基類的版本不參與重載?。?!

所以,在派生類中想增加還是改寫無(wú)論virtual還是non-virtual函數(shù)的重載版本,第一件事是使用using聲明將基類符號(hào)的所有版本聲明到派生的名字作用域中,然后再干其他的。

3.2 絕不重新定義繼承而來的non-virtual函數(shù)

C++的non-virtual函數(shù)都是編譯期靜態(tài)綁定的,其名字查找從其指針的靜態(tài)類型開始。

任何情況下,都不要重新定義一個(gè)繼承而來的non-virtual函數(shù),否則其調(diào)用的版本決定于其指針靜態(tài)類型,這與public繼承is-a的一致性關(guān)系相互違背。

3.3 絕不重新定義繼承來的缺省參數(shù)值

因?yàn)樯厦嬲f到我們不應(yīng)該重新定義一個(gè)繼承而來的non-virtual函數(shù),所以到這里我們可以說:絕對(duì)不要重新定義一個(gè)繼承而來帶缺省參數(shù)值的virtual函數(shù)的參數(shù)默認(rèn)值。其原因是:virtual函數(shù)是動(dòng)態(tài)綁定的,而缺省參數(shù)是靜態(tài)綁定的。

所以如果基類和派生類的參數(shù)默認(rèn)值不一致,則使用引用、指針調(diào)用發(fā)生參數(shù)默認(rèn)值靜態(tài)綁定和調(diào)用函數(shù)體動(dòng)態(tài)綁定將會(huì)非常的詭異,所以需要避免這種情況。還有就是如果虛函數(shù)參數(shù)再基類指定的參數(shù)缺省值,而派生類override的時(shí)候沒有指明參數(shù)缺省值,此時(shí)如果客戶端以派生類對(duì)象方式調(diào)用該函數(shù),則發(fā)生的是靜態(tài)綁定,需要顯示指定參數(shù)值;而如果客戶端以指針、引用的新式調(diào)用該函數(shù),則發(fā)生的是動(dòng)態(tài)綁定,可以不指定其帶有缺省值的參數(shù)。

class Shape {
public: virtual void draw(ShapeColor color = Red) const = 0; ...
};
class Circle: public Shape {
// 如果以對(duì)象模式調(diào)用draw,必須指定color參數(shù)而不能使用缺省參數(shù)
public: virtual void draw(ShapeColor color) const; ... 
};

解決這個(gè)問題的一個(gè)方式是使用NVI手法,其public non-virtual接口提供默認(rèn)默認(rèn)值(且不會(huì)被派生類重寫),而private virtual不使用默認(rèn)默認(rèn)的特性以規(guī)避這種可能的不一致性。

3.4 private繼承

private繼承沒有”is-a”的契約關(guān)系了,在使用上一個(gè)巨大的差異是:編譯器不再會(huì)自動(dòng)將一個(gè)派生類對(duì)象轉(zhuǎn)換為一個(gè)基類對(duì)象了,這意味著原本接收基類對(duì)象的函數(shù)參數(shù)將不再能夠?yàn)槠鋫鬟f派生類對(duì)象作為實(shí)參了(對(duì)象、引用、指針類型都不允許,編譯器會(huì)報(bào)基類S是派生類T不可訪問的基類);同時(shí)由基類繼承而來的所有成員,在派生類中都會(huì)變成private的訪問權(quán)限。

private繼承意味著只有實(shí)現(xiàn)部分被繼承,接口部分被全部略去了,所以private繼承應(yīng)當(dāng)是采用基類的某些功能幫助派生類完善其功能,從某種情況下說具有”has-a”的符合類型,所以除了考慮到派生類需要訪問基類protected成員和virtual的因素被牽扯進(jìn)來,否則應(yīng)該盡量使用組合類型來代替private繼承,而且即使如此,也可以使用下面的手法瞞天過海:

class Timer { 
public: virtual void onTick() const; ... 
};
class Widget {
private:
 class WidgetTimer: public Timer {
 public: virtual void OnTick() const; ...
 };
 WidgetTimer timer;
};

關(guān)于protected繼承,連Meyers大神都沒用過,那么我又何必廢腦經(jīng)去考慮他……

參考

Effective C++

總結(jié)

以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI