C++語言常見問題解:#54 ~ #80

這是我從臺灣的http://www.cis.nctu.edu.tw/chinese/doc/research/c++/C++FAQ-Chinese/發現的《C++ Frequently Asked Questions》的繁體翻譯,作者是:葉秉哲,也是《C++ Programming Language》3/e繁體版的譯者,該文章是非常的好,出於學習用途而將它轉貼,本人未取得作者的授權,原文章的版權仍然歸屬原作者.

C++語言常見問題解
Q54:Derived* --> Base* 是正常的;那爲什麼 Derived** --> Base** 則否?

C++ 讓 Derived* 能轉型到 Base*,是因爲衍生的對象「是一種」基底的對象。然而
想由 Derived** 轉型到 Base** 則是錯誤的!要是能夠的話,Base** 就可能會被解
參用(產生一個 Base*),該 Base* 就可能指向另一個“不一樣的”衍生類別,這
是不對的。

照此看來,衍生類別的數組就「不是一種」基底類別的數組。在 Paradigm Shift 公
司的 C++ 訓練課程裏,我們用底下的例子來比喻:

 "一袋蘋果「不是」一袋水果".
"A bag of apples is NOT a bag of fruit".

如果一袋蘋果可以當成一袋水果來傳遞,別人就可能把香蕉放到蘋果袋裏頭去!

========================================

Q55:衍生類別的數組「不是」基底的數組,是否表示數組不好?

沒錯,「數組很爛」(開玩笑的 Smile

C++ 內建的數組有一個不易察覺的問題。想一想:

void f(Base* arrayOfBase)
{
arrayOfBase[3].memberfn();
}

main()
{
Derived arrayOfDerived[10];
f(arrayOfDerived);
  }

編譯器認爲這完全是型別安全的,因爲由 Derived* 轉換到 Base* 是正常的。但事
實上這很差勁:因爲 Derived 可能會比 Base 還要大,f() 裏頭的數組索引不光是
沒有型別安全,甚至還可能沒指到真正的對象呢!通常它會指到某個倒黴的
Derived 對象的中間去。

根本的問題在於:C++ 不能分辨出「指向一個東西」和「指向一個數組」。很自然的
,這是 C++“繼承”自 C 語言的特徵。

注意:如果我們用的是一個像數組的「類別」而非最原始的數組(譬如:"Array<T>"
而非 "T[]"),這問題就可以在編譯期被挑出來,而非在執行的時候。

==========================
● 12A:繼承--虛擬函數
==========================

Q56:什麼是「虛擬成員函數」?

虛擬函數可讓衍生的類別「取代」原基底類別所提供的運作。只要某對象是衍生出來
的,就算我們是透過基底對象的指針,而不是以衍生對象的指針來存取該對象,編譯
器仍會確保「取代後」的成員函數被呼叫。這可讓基底類別的算法被衍生者所替換
,即使我們不知道衍生類別長什麼樣子。

注意:衍生的類別亦可“部份”取代(覆蓋,override)掉基底的運作行爲(如有必
要,衍生類別的運作行爲亦可呼叫它的基底類別版本)。

========================================

Q57:C++ 怎樣同時做到動態繫結和靜態型別?

底下的討論中,"ptr" 指的是「指針」或「參考」。

一個 ptr 有兩種型態:靜態的 ptr 型態,與動態的「被指向的對象」的型態(該物
件可能實際上是個由其它類別衍生出來的類別的 ptr)。

「靜態型別」("static typing") 是指:該呼叫的「合法性」,是以 ptr 的靜態型
別爲偵測之依據,如果 ptr 的型別能處理成員函數,則「指向的對象」自然也能。

「動態繫結」("dynamic binding") 是指:「程序代碼」呼叫是以「被指向的對象」之
型態爲依據。被稱爲「動態繫結」,是因爲真正會被呼叫的程序代碼是動態地(於執行
時期)決定的。

========================================

Q58:衍生類別能否將基底類別的非虛擬函數覆蓋(override)過去?

可以,但不好。

C++ 的老手有時會重新定義非虛擬的函數,以提升效率(換一種可能會運用到衍生類
別纔有的資源的作法),或是用以避開遮蔽效應(hiding rule,底下會提,或是看
看 ARM ["Annotated Reference Manual"] sect.13.1),但是用戶的可見性效果必
須完全相同,因爲非虛擬的函數是以指針/參考的靜態型別爲分派(dispatch)的依
據,而非以指到的/被參考到的對象之動態型別來決定。

========================================

Q59:"Warning: Derived::f(int) hides Base::f(float)" 是什麼意思?

這是指:你死不了的。

你出的問題是:如果 Derived 宣告了個叫做 "f" 的成員函數,Base 卻早已宣告了
個不同型態簽名型式(譬如:參數型態或是 const 不同)的 "f",這樣子 Base "f"
就會被「遮蔽 hide」住,而不是被「多載 overload」或「覆蓋 override」(即使
Base "f" 已經是虛擬的了)。

解決法:Derived 要替 Base 被遮蔽的成員函數重新定義(就算它不是虛擬的)。通
常重定義的函數,僅僅是去呼叫合適的 Base 成員函數,譬如:

class Base {
public:
void f(int);
};

class Derived : public Base {
public:
void f(double);
void f(int i) { Base::f(i); }
}; // ^^^^^^^^^^--- 重定義的函數只是去呼叫 Base::f(int)

========================
● 12B:繼承--一致性
========================

Q60:我該遮蔽住由基底類別繼承來的公共成員函數嗎?

絕對絕對絕對絕對不要這樣做!

想去遮蔽(刪去﹑撤消)掉繼承下來的公共成員函數,是個很常見的錯誤。這通常是
腦袋塞滿了漿糊的人才會做的傻事。

========================================

Q61:圓形 "Circle" 是一種橢圓 "Ellipse" 嗎?

若橢圓能夠不對稱地改變其兩軸的大小,則答案就是否定的。

比方說,橢圓有個 "setSize(x,y)" 的運作行爲,且它保證說「橢圓的 width() 爲
x,height() 爲 y」。這種情況之下,正圓形就不能算是一種橢圓。因爲只要把某個
橢圓能做而正圓形不能的東西放進去,圓形就不再是個橢圓了。

這樣一來,圓和橢圓之間可能有兩種的(合法)關係:
* 將圓與橢圓完全分開來談。
* 讓圓及橢圓都同時自一個基底衍生出來,該基底爲「不能做不對稱的 setSize
運作的特殊橢圓形」。

以第一個方案而言,橢圓可繼承自「非對稱圖形」(伴隨着一個 setSize(x,y) ),
圓形則繼承自「對稱圖形」,帶有一個 setSize(size) 成員函數。

第二個方案中,可讓卵形 "Oval" 類別有個 "setSize(size)":將 "width()" 和
"height()" 都設成 "size",然後讓橢圓和圓形都自卵形中衍生出來。橢圓(而不是
正圓形)會加入一個 "setSize(x,y)" 運算(如果這個 "setSize()" 運作行爲的名
稱重複了,就得注意前面提過的「遮蔽效應」)。

========================================

Q62:對「圓形是/不是一種橢圓」這兩難問題,有沒有其它說法?

如果你說:橢圓都可以不對稱地擠壓,又說:圓形是一種橢圓,又說:圓形不能不對
稱地擠壓下去,那麼很明顯的,你說過的某句話要做修正(老實說,該取消掉)。所
以你不是得去掉 "Ellipse::setSize(x,y)",去掉圓形和橢圓間的繼承關係,就是得
承認你的「圓形」不一定是正圓。

這兒有兩個 OO/C++ 新手最易落入的陷阱。他們想用程序小技巧來彌補差勁的事前設
計(他們重新定義 Circle::setSize(x,y),讓它丟出一個例外,呼叫 "abort()" ,
或是選用兩參數的平均數,或是不做任何事情),不幸的,這些技倆都會讓使用者感
到喫驚:他們原本都預期 "width() == x" 和 "height() == y" 這兩個事實會成立。

唯一合理的做法似乎是:降低橢圓形 "setSize(x,y)" 的保證事項(譬如,你可以改
成:「這運作行爲“可能”會把 width() 設成 x﹑height() 設成 y,也可能“不做
任何事”」)。不幸的,這樣會把界限沖淡,因爲使用者沒有任何有意義的對象行爲
足以依靠,整個類別階層也就無毫價值可言了(很難說服別人去用一個:問你說它是
做什麼的,你卻只會聳聳肩膀說不知道的對象)。

==========================
● 12C:繼承--存取規則
==========================

Q63:爲什麼衍生的類別無法存取基底的 "private" 東西?

讓你不被基底類別將來的改變所影響。

衍生類別不能存取到基底的私有(private)成員,它有效地把衍生類別「封住」,
基底類別內的私有成員如有改變,也不會影響到衍生的類別。

========================================

Q64:"public:"﹑"private:"﹑"protected:" 的差別是?

"Private:" 在前幾節中討論過了;"public:" 是指:「任何人都能存取之」;第三
個 "protected:" 是讓某成員(資料成員或是成員函數)只能由衍生類別存取之。

【譯註】"protected:" 是讓「衍生類別」,而非讓「衍生類別的對象案例」能存取
    得到 protected 的部份。

========================================

Q65:當我改變了內部的東西,怎樣避免子類別被破壞?

對象類別有兩個不同的接口,提供給不同種類的用戶:
* "public:" 接口用以服務不相關的類別。
* "protected:" 接口用以服務衍生的類別。

除非你預期所有的子類別都會由你們的工作小組建出來,否則你應該將基底類別的資
料位內容放在 "private:" 處,用 "protected:" 行內存取函數來存取那些數據。
這樣的話,即使基底類別的私有資料改變了,衍生類別的程序也不會報廢,除非你改
變了基底類別的 protected 處的存取函數。

================================
● 12D:繼承--建構子與解構子
================================

Q66:若基底類別的建構子呼叫一個虛擬函數,爲什麼衍生類別覆蓋掉的那個虛擬函
數卻不會被呼叫到?

在基底類別 Base 的建構子執行過程中,該對象還不是屬於衍生 Derived 的,所以
如果 "Base::Base()" 呼叫了虛擬函數 "virt()",則 "Base::virt()" 會被呼叫,
即使真的有 "Derived::virt()"。

類似的道理,當 Base 的解構子執行時,該對象不再是個 Derived 了,所以當
Base::~Base() 呼叫 "virt()",則 "Base::virt()" 會被執行,而非覆蓋後的版本
"Derived::virt()"。

當你想象到:如果 "Derived::virt()" 碰得到 Derived 類別的對象成員,會造成什
麼樣的災難,你很快就會看出這規則的明智之處。

================================

Q67:衍生類別的解構子應該外顯地呼叫基底的解構子嗎?

不要,絕對不要外顯地呼叫解構子(「絕對不要」指的是「幾乎完全不要」)。

衍生類別的解構子(不管你是否明顯定義過)會“自動”去呼叫成員對象的﹑以及基
底類別之子對象的解構子。成員對象會以它們在類別中出現的相反順序解構,接下來
是基底類別的子對象,以它們出現在類別基底列表的相反順序解構之。

只有在極爲特殊的情況下,你才應外顯地呼叫解構子,像是:解構一個由「新放入的
new 運操作數」配置的對象。

===========================================
● 12E:繼承--Private 與 protected 繼承
===========================================

Q68:該怎麼表達出「私有繼承」(private inheritance)?

用 ": private" 來代替 ": public." 譬如:

    class Foo : private Bar {
//...
};

================================

Q69:「私有繼承」和「成份」(composition) 有多類似?

私有繼承是「成份」(has-a) 的一種語法變形。

譬如:「汽車有引擎」("car has-a engine") 關係可用成份來表達:

class Engine {
public:
Engine(int numCylinders);
void start(); //starts this Engine
};

class Car {
public:
Car() : e_(8) { } //initializes this Car with 8 cylinders
void start() { e_.start(); } //start this Car by starting its engine
private:
Engine e_;
};

同樣的 "has-a" 關係也可用私有繼承來表達:

class Car : private Engine {
public:
Car() : Engine(8) { } //initializes this Car with 8 cylinders
Engine::start; //start this Car by starting its engine
};

這兩種型式的成份有幾分相似性:
* 這兩種情況之下,Car 只含有一個 Engine 成員對象。
* 兩種情況都不能讓(外界)使用者由 Car* 轉換成 Engine* 。

也有幾個不同點:
* 如果你想要讓每個 Car 都含有數個 Engine 的話,就得用第一個型式。
* 第二個型式可能會導致不必要的多重繼承(multiple inheritance)。
* 第二個型式允許 Car 的成員從 Car* 轉換成 Engine* 。
* 第二個型式可存取到基底類別的 "protected" 成員。
* 第二個型式允許 Car 覆蓋掉 Engine 的虛擬函數。

注意:私有繼承通常是用來獲得基底類別 "protected:" 成員的存取權力,但這通常
只是個短程的解決方案。

========================================

Q70:我比較該用哪一種:成份還是私有繼承?

成份。

正常情形下,你不希望存取到太多其它類別的內部,但私有繼承會給你這些額外的權
力(與責任)。不過私有繼承不是洪水猛獸;它只是得多花心力去維護罷了,因爲它
增加了別人動到你的東西﹑讓你的程序出差錯的機會。

合法而長程地使用私有繼承的時機是:當你想新建一個 Fred 類別,它會用到 Wilma
類別的程序代碼,而且 Wilma 的程序代碼也會呼叫到你這個 Fred 類別裏的運作行爲時
。這種情形之下,Fred 呼叫了 Wilma 的非虛擬函數,Wilma 也呼叫了它自己的﹑會
被 Fred 所覆蓋的虛擬函數(通常是純虛擬函數)。要用成份來做的話,太難了。

class Wilma {
protected:
void fredCallsWilma()
{ cout << "Wilma::fredCallsWilma()/n"; wilmaCallsFred(); }
virtual void wilmaCallsFred() = 0;
};

class Fred : private Wilma {
public:
void barney()
{ cout << "Fred::barney()/n"; Wilma::fredCallsWilma(); }
protected:
virtual void wilmaCallsFred()
{ cout << "Fred::wilmaCallsFred()/n"; }
};

========================================

Q71:我應該用指針轉型方法,把「私有」衍生類別轉成它的基底嗎?

當然不該。

以私有衍生類別的運作行爲﹑夥伴來看,從它上溯到基底類別的關係爲已知的,所以
從 PrivatelyDer* 往上轉換成 Base*(或是從 PrivatelyDer& 到 Base&)是安全的
;強制轉型是不需要也不鼓勵的。

然而用 PrivateDer 的人應該避免這種不安全的轉換,因爲此乃立足於 PrivateDer
的 "private" 決定,這個決定很容易在日後不經察覺就改變了。

========================================

Q72:保護繼承 (protected inheritance) 和私有繼承有何關連?

相似處:兩者都能覆蓋掉私有/保護基底類別的虛擬函數,兩者都不把衍生的類別視
爲“一種”基底類別。

不相似處:保護繼承可讓衍生類別的衍生類別知道它的繼承關係(把實行細節顯現出
來)。它有好處(允許保護繼承類別的子類別,藉這項關係來使用保護基底類別),
也有代價(保護繼承的類別,無法既想改變這種關係,而又不破壞到進一步的衍生類
別)。

保護繼承使用 ": protected" 這種語法:

class Car : protected Engine {
//...
};

========================================

Q73:"private" 和 "protected" 的存取規則是什麼?

拿底下這些類別當例子:

class B { /*...*/ };
class D_priv : private B { /*...*/ };
class D_prot : protected B { /*...*/ };
class D_publ : public B { /*...*/ };
class UserClass { B b; /*...*/ };

沒有一個子類別能存取到 B 的 private 部份。
在 D_priv 內,B 的 public 和 protected 部份都變成 "private"。
在 D_prot 內,B 的 public 和 protected 部份都變成 "protected"。
在 D_publ 內,B 的 public 部份還是 public,protected 還是 protected
(D_publ is-a-kind-of-a B) 。
Class "UserClass" 只能存取 B 的 public 部份,也就是:把 UserClass 從 B 那
兒封起來了。

欲把 B 的 public 成員在 D_priv 或 D_prot 內也變成 public,只要在該成員的名
字前面加上 "B::"。譬如:想讓 "B::f(int,float)" 成員在 D_prot 內也是 public
的話,照這樣寫:

class D_prot : protected B {
public:
B::f; //注意:不是寫成 "B::f(int,float)"
};


======================================
■□ 第13節:抽象化(abstraction)
======================================

Q74:分離接口與實作是做什麼用的?

接口是企業體最有價值的資源。設計接口會比只把一堆獨立的類別拼湊起來來得耗時
,尤其是:接口需要花費更高階人力的時間。

既然接口是如此重要,它就應該保護起來,以避免被數據結構等等實作細節之變更所
影響。因此你應該將接口與實作分離開來。

========================================

Q75:在 C++ 裏,我該怎樣分離接口與實作(像 Modula-2 那樣)?

用 ABC(見下一則 FAQ)。

========================================

Q76:ABC ("abstract base class") 是什麼?

在設計層面,ABC 對應到抽象的概念。如果你問機械師父說他修不修運輸工具,他可
能會猜你心中想的到底是“哪一種”運輸工具,他可能不會修理航天飛機﹑輪船﹑腳踏
車﹑核子潛艇。問題在於:「運輸工具」是個抽象的概念(譬如:你建不出一輛「運
輸工具」,除非你知道要建的是“哪一種”)。在 C++,運輸工具類別可當成是一個
ABC,而腳踏車﹑航天飛機……等等都當做它的子類別(輪船“是一種”運輸工具)。
在真實世界的 OOP 中,ABC 觀念到處都是。

在程序語言層面,ABC 是有一個以上純虛擬成員函數(pure virtual)的類別(詳見
下一則 FAQ),你無法替一個 ABC 建造出對象(案例)來。

========================================

Q77:「純虛擬」(pure virtual) 成員函數是什麼?

ABC 的某種成員函數,你只能在衍生的類別中實作它。

有些成員函數只存於觀念中,沒有任何實質的定義。譬如,假設我要你畫個 Shape,
它位於 (x,y),大小爲 7。你會問我「我該畫哪一種 shape?」(圓﹑方﹑六邊……
都有不同的畫法。)在 C++ 裏,我們可以先標出有一個叫做 "draw()" 這樣的運作
行爲,且規定它只能(邏輯上)在子類別中定義出來:

class Shape {
public:
virtual void draw() const = 0;
//... ^^^--- "= 0" 指:它是 "pure virtual"
};

此純虛擬函數讓 "Shape" 變成一個 ABC。若你願意,你可以把 "= 0" 語法想成是:
該程序代碼是位於 NULL 指針處。因此,"Shape" 提供一個服務項目,但它現在尚無法
提供實質的程序代碼以實現之。這樣會確保:任何由 Shape 衍生出的 [具體的] 類別
之對象,“將會”有那個我們事先規定的成員函數,即使基底類別尚無足夠的信息去
真正的“定義”它。

【譯註】此處「定義」、「宣告」二詞要分辨清楚!

========================================

Q78:怎樣替整個類別階層提供打印的功能?

提供一個 friend operator<< 去呼叫 protected 的虛擬函數:

class Base {
public:
friend ostream& operator<< (ostream& o, const Base& b)
{ b.print(o); return o; }
//...
protected:
virtual void print(ostream& o) const; //或 "=0;" 若 "Base" 是個 ABC
};

class Derived : public Base {
protected:
virtual void print(ostream& o) const;
};

這樣子所有 Base 的子類別只須提供它們自己的 "print(ostream&) const" 成員函
數即可(它們都共享 "<<" operator)。這種技巧讓夥伴像是有了動態繫結的能力。

========================================

Q79:何時該把解構子弄成 virtual?

當你可能經由基底的指針去 "delete" 掉衍生的類別時。

虛擬函數把某對象所屬之真正類別所附的程序代碼,而非該指針/參考本身之類別所附
的程序給繫結上去。 當你說 "delete basePtr",且它的基底有虛擬解構子的話,則
真正會被呼叫到的解構子,就是 *basePtr 對象之型態所屬的解構子,而不是該指針
本身之型態所附的解構子。一般說來這的確是一件好事。

讓你方便起見,你唯一不必將某類別的解構子設爲 virtual 的場合是:「該類別“
沒有”任何虛擬函數」。因爲加入第一個虛擬函數,就會替每個對象都添加額外的空
間負擔(通常是一個機器 word 的大小),這正是編譯器實作出動態繫結的祕密;它
通常會替每個對象加入額外的指針,稱爲「虛擬指針表格」(virtual table pointer)
,或是 "vptr" 。

========================================

Q80:虛擬建構子 (virtual constructor) 是什麼?

一種讓你能做些 C++ 不直接支持的事情之慣用法。

欲做出虛擬建構子的效果,可用個虛擬的 "createCopy()" 成員函數(用來做爲拷貝
建構子),或是虛擬的 "createSimilar()" 成員函數(用來做爲預設建構子)。

class Shape {
public:
virtual ~Shape() { } //詳見 "virtual destructors"
virtual void draw() = 0;
virtual void move() = 0;
//...
virtual Shape* createCopy() const = 0;
virtual Shape* createSimilar() const = 0;
};

class Circle : public Shape {
public:
Circle* createCopy() const { return new Circle(*this); }
Circle* createSimilar() const { return new Circle(); }
//...
};

執行了 "Circle(*this)" 也就是執行了拷貝建構的行爲(在這些運作行爲中,
"*this" 的型態爲 "const Circle&")。"createSimilar()" 亦類似,但它乃建構出
一個“預設的”Circle。

這樣用的話,就如同有了「虛擬建構子」(virtual constructors):

void userCode(Shape& s)
{
Shape* s2 = s.createCopy();
Shape* s3 = s.createSimilar();
//...
delete s2; // 該解構子必須是 virtual 纔行!!
delete s3; // 如上.
}

不論該 Shape 是 Circle﹑Square,甚或其它還不存在的 Shape 種類,這函數都能
正確執行。

--
Marshall Cline
--
Marshall P. Cline, Ph.D. / Paradigm Shift Inc / PO Box 5108 / Potsdam NY 13676
[email protected] / 315-353-6100 / FAX: 315-353-6110



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