全國最多中醫師線上諮詢網站-台灣中醫網
發文 回覆 瀏覽次數:1894
推到 Plurk!
推到 Facebook!

中文C++FAQ

 
jackkcg
站務副站長


發表:891
回覆:1050
積分:848
註冊:2002-03-23

發送簡訊給我
#1 引用回覆 回覆 發表時間:2002-09-10 23:57:20 IP:61.221.xxx.xxx 未訂閱
中文版(Chinese Version) 英文版(English Version) http://www.cis.nctu.edu.tw/chinese/doc/research/doc/c3p4.html -------------------------------------------------------------------------------- 以上英文原作者為 Marshall Cline, 中譯者為葉秉哲. 翻譯文件為 USENET comp. lang, c FAQ, Jan31, 1996 == Part 2/4 ============================ comp.lang.c Frequently Asked Questions list (with answers, fortunately). Copyright (C) 1991-96 Marshall P. Cline, Ph.D. Posting 2 of 4. Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc ============================= ■□ 第9節:自由記憶體管理 ============================= Q33:"delete p" 會刪去 "p" 指標,還是它指到的資料,"*p" ? 該指標指到的資料。 "delete" 真正的意思是:「刪去指標所指到的東西」(delete the thing pointed to by)。同樣的英文誤用也發生在 C 語言的「『釋放』指標所指向的記憶體」上 ("free(p)" 真正的意思是:"free_the_stuff_pointed_to_by(p)" )。 ======================================== Q34:我能 "free()" 掉由 "new" 配置到的、"delete" 掉由 "malloc()" 配置到的 記憶體嗎? 不行。 在同一個程式裡,使用 malloc/free 及 new/delete 是完全合法、合理、安全的; 但 free 掉由 new 配置到的,或 delete 掉由 malloc 配置到的指標則是不合法、 不合理、該被痛罵一頓的。 ======================================== Q35:為什麼該用 "new" 而不是老字號的 malloc() ? 建構子/解構子、型別安全性、可被覆蓋(overridability)。 建構子/解構子:和 "malloc(sizeof(Fred))" 不同,"new Fred()" 還會去呼叫 Fred 的建構子。同理,"delete p" 會去呼叫 "*p" 的解構子。 型別安全性:malloc() 會傳回一個不具型別安全的 "void*",而 "new Fred()" 則 會傳回正確型態的指標(一個 "Fred*")。 可被覆蓋:"new" 是個可被物件類別覆蓋的運算子,而 "malloc" 不是以「各個類別 」作為覆蓋的基準。 ======================================== Q36:為什麼 C 不替 "new" 及 "delete" 搭配個 "realloc()" ? 避免你產生意外。 當 realloc() 要拷貝配置區時,它做的是「逐位元 bitwise」的拷貝,這會弄壞大 部份的 C 物件。不過 C 的物件應該要能自我拷貝才對:用它們自己的拷貝建構 子或設定運算子。 ======================================== Q37:我該怎樣配置/釋放陣列? 用 new[] 和 delete[] : Fred* p = new Fred[100]; //... delete [] p; 每當你在 "new" 運算式中用了 "[...]",你就必須在 "delete" 陳述中使用 "[]"。 ^^^^ 這語法是必要的,因為「指向單一元素的指標」與「指向一個陣列的指標」在語法上 並無法區分開來。 ======================================== Q38:萬一我忘了將 "[]" 用在 "delete" 由 "new Fred[n]" 配置到的陣列,會發生 什麼事? 災難。 這是程式者的--而不是編譯器的--責任,去確保 new[] 與 delete[] 的正確配 對。若你弄錯了,編譯器不會產生任何編譯期或執行期的錯誤訊息。堆積(heap)被 破壞是最可能的結局,或是更糟的,你的程式會當掉。 ======================================== Q39:成員函數做 "delete this" 的動作是合法的(並且是好的)嗎? 只要你小心的話就沒事。 我所謂的「小心」是: 1) 你得 100% 確定 "this" 是由 "new" 配置來的(而非 "new[]",亦非自訂的 "new" 版本,一定要是最原始的 "new")。 2) 你得 100% 確定該成員函數是此物件最後一個會呼叫到的。 3) 做完自殺的動作 ("delete this;") 後,你不能再去碰 "this" 的物件了,包 括資料及運作行為在內。 4) 做完自殺的動作 ("delete this;") 後,你不能再去碰 "this" 指標了。 換句話說,你不能查看它﹑將它與其他指標或是 NULL 相比較﹑印出其值﹑ 對它轉型﹑對它做任何事情。 很自然的,這項警告也適用於:當 "this" 是個指向基底類別的指標,而解構子不是 virtual 的場合。 ======================================== Q40:我該怎麼用 new 來配置多維陣列? 有很多方法,端視你對陣列大小的伸縮性之要求而定。極端一點的情形,如果你在編 譯期就知道所有陣列的維度,你可以靜態地配置(就像 C 一樣): class Fred { /*...*/ }; void manipulateArray() { Fred matrix[10][20]; //使用 matrix[i][j]... //不須特地去釋放該陣列 } 另一個極端情況,如果你希望該矩陣的每個小塊都能不一樣大,你可以在自由記憶體 裡配置之: void manipulateArray(unsigned nrows, unsigned ncols[]) //'nrows' 是該陣列之列數。 //所以合法的列數為 (0, nrows-1) 開區間。 //'ncols[r]' 則是 'r' 列的行數 ('r' 值域為 [0..nrows-1])。 { Fred** matrix = new Fred*[nrows]; for (unsigned r = 0; r < nrows; r) matrix[r] = new Fred[ ncols[r] ]; //使用 matrix[i][j]... //釋放就是配置的反動作: for (r = nrows; r > 0; --r) delete [] matrix[r-1]; delete [] matrix; } ======================================== Q41:C 能不能做到在執行時期才指定陣列的長度? 可以。STL 有一個 vector template 提供這種行為。請參考“程式庫”一節的 STL 項目。 不行。內建的陣列型態必須在編譯期就指定它的長度了。 可以,內建的陣列可以在執行期才指定第一個索引的範圍。譬如說,和上一則 FAQ 相較,如果你只需要第一個維度大小能夠變動,你可以 new 一個陣列的陣列(而不 是陣列指標的陣列 "an array of pointers to arrays"): const unsigned ncols = 100; //'ncols' 不是執行期才決定的變數 (用來代表陣列的行數) class Fred { ... }; void manipulateArray(unsigned nrows) //'nrows' 是執行期才決定的變數 (用來代表陣列的列數) { Fred (*matrix)[ncols] = new Fred[nrows][ncols]; //用 matrix[i][j] 來處理 //deletion 是物件配置的逆運算: delete [] matrix; } 如果你不光是需要在執行期改變陣列的第一個維度的話,就不能這樣做了。 ======================================== Q42:怎樣確保某類別的物件都是用 "new" 建立的,而非區域或整體/靜態變數? 確定該類別的建構子都是 "private:" 的,並定義個 "friend" 或 "static" 函數, 來傳回一個指向由 "new" 建造出來的物件(把建構子設成 "protected:",如果你想 要有衍生類別的話)。 class Fred { //只允許 Fred 動態地配置出來 public: static Fred* create() { return new Fred(); } static Fred* create(int i) { return new Fred(i); } static Fred* create(const Fred& fred) { return new Fred(fred); } private: Fred(); Fred(int i); Fred(const Fred& fred); virtual ~Fred(); }; main() { Fred* p = Fred::create(5); ... delete p; } =============================== ■□ 第10節:除錯與錯誤處理 =============================== Q43:怎樣處理建構子的錯誤? 丟出一個例外(throw an exception)。 建構子沒有傳回值,所以不可能採用它傳回的錯誤碼。因此,偵測建構子錯誤最好的 方法,就是丟出一個例外。 在 C 編譯器尚未提供例外處理之前,我們可先把物件置於「半熟」的狀態(譬如 :設個內部的狀態位元),用個查詢子("inspector")來檢查該位元,就可讓用戶 查看該物件是否還活著。也可以用另一個成員函數來檢查該位元,若該物件沒存活 下來,就做個「沒動作」(或是更狠的像是 "abort()" )的程式。但這實在很醜陋。 ======================================== Q44:如果建構子會丟出例外的話,該怎麼處理它的資源? 物件裡面的每個資料成員,都該自己收拾殘局。 如果建構子丟出一個例外的話,該物件的解構子就“不會”執行。如果你的物件得回 復些曾做過的事情(像是配置記憶體、開啟檔案、鎖定 semaphore),該物件內的資 料成員就“必須”記住這個「必須恢復的東西」。 舉例來說:不要單單的把配置到的記憶體放入 "Fred*" 資料成員,而要放入一個「 聰明的指標」(smart pointer) 資料成員中;當該“聰明指標”死掉的話,它的解構 子就會刪去 Fred 物件。 【譯註】「聰明的指標」(smart pointer) 在 Q4 中有提到一點。 ============================= ■□ 第11節:Const 正確性 ============================= Q45:什麼是 "const correctness"? 好問題。 「常數正確性」乃使用 "const" 關鍵字,以確保常數物件不會被更動到。譬如:若 "f()" 函數接收一個 "String",且 "f()" 想確保 "String" 不會被改變,你可以: * 傳值呼叫 (pass by value): void f( String s ) { /*...*/ } * 透過常數參考 (reference): void f(const String& s ) { /*...*/ } * 透過常數指標 (pointer) : void f(const String* sptr) { /*...*/ } * 但不能用非常數參考 : void f( String& s ) { /*...*/ } * 也不能用非常數指標 : void f( String* sptr) { /*...*/ } 在接收 "const String&" 參數的函數裡面,想更動到 "s" 的話,會產生個編譯期的 錯誤;沒有犧牲任何執行期的空間及速度。 宣告 "const" 參數也是另一種型別安全方法,就像一個常數字串,它會“喪失”各 種可能會變更其內容的行為動作。如果你發現型別安全性質讓你的系統正確地運作 (這是真的;特別是大型的系統),你會發現「常數正確性」亦如是。 ======================================== Q46:我該早一點還是晚一點讓東西有常數正確性? 越越越早越好。 延後補以常數正確性,會導致雪球效應:每次你在「這兒」用了 "const",你就得在 「那兒」加上四個以上的 "const"。 ======================================== Q47:什麼是「const 成員函數」? 一個只檢測(而不更動)其物件的成員函數。 class Fred { public: void f() const; }; // ^^^^^--- 暗示說 "fred.f()" 不會改變到 "fred" 此乃意指:「抽象層次」的(用戶可見的)物件狀態不被改變(而不是許諾:該物件 的「每一個位元內容」都不會被動到)。C 編譯器不會對你許諾「每一個位元」這 種事情,因為不是常數的別名(alias)就可能會修改物件的狀態(把 "const" 指標 黏上某個物件,並不能擔保該物件不被改變;它只能擔保該物件不會「被該指標的動 作」所改變)。 【譯註】請逐字細讀上面這句話。 "const" 成員函數常被稱作「查詢子」(inspector),不是 "const" 的成員函數則 稱為「更動子」(mutator)。 ======================================== Q48:若我想在 "const" 成員函數內更新一個「看不見的」資料成員,該怎麼做? 使用 "mutable" 或是 "const_cast"。 【譯註】這是很新的 ANSI C RTTI (RunTime Type Information) 規定,Borland C 4.0 就率先提供了 const_cast 運算子。 少數的查詢子需要對資料成員做些無害的改變(譬如:"Set" 物件可能想快取它上一 回所查到的東西,以加速下一次的查詢)。此改變「無害」是指:此改變不會由物件 的外部介面察覺出來(否則,該運作行為就該叫做更動子,而非查詢子了)。 這類情況下,會被更動的資料成員就該被標示成 "mutable"(把 "mutable" 關鍵字 放在該資料成員宣告處前面;也就是和你放 "const" 一樣的地方),這會告訴編譯 器:此資料成員允許 const 成員函數改變之。若你不能用 "mutable" 的話,可以用 "const_cast" 把 "this" 的「常數性」給轉型掉。譬如,在 "Set::lookup() const" 裡,你可以說: Set* self = const_cast(this); 這行執行之後,"self" 的位元內容就和 "this" 一樣(譬如:"self==this"),但 是 "self" 是一個 "Set*" 而非 "const Set*" 了,所以你就可以用 "self" 去修改 "this" 指標所指向的物件。 ======================================== Q49:"const_cast" 會不會喪失最佳化的可能? 理論上,是;實際上,否。 就算編譯器沒真正做好 "const_cast",欲避免 "const" 成員函數被呼叫時,會造成 暫存器快取區被清空的唯一方法,乃確保沒有任何「非常數」的指標指向該物件。這 種情況很難得會發生(當物件在 const 成員函數被啟用的範圍內被建立出來;當所 有非 const 的成員函數在物件建立間啟用,和 const 成員函數的啟用被靜態繫結住 ;當所有的啟用也都是 "inline";當建構子本身就是 "inline";和當建構子所呼叫 的任何成員函數都是 inline 時)。 【譯註】這一段話很難翻得好(好啦好啦!我功力不足... :-< ),所以附上原文: Even if a compiler outlawed "const_cast", the only way to avoid flushing the register cache across a "const" member function call would be to ensure that there are no non-const pointers that alias the object. This can only happen in rare cases (when the object is constructed in the scope of the const member fn invocation, and when all the non-const member function invocations between the object's construction and the const member fn invocation are statically bound, and when every one of these invocations is also "inline"d, and when the constructor itself is "inline"d, and when any member fns the constructor calls are inline). ===================== ■□ 第12節:繼承 ===================== Q50:「繼承」對 C 來說很重要嗎? 是的。 「繼承」是抽象化資料型態(abstract data type, ADT)與 OOP 的一大分野。 ======================================== Q51:何時該用繼承? 做為一個「特異化」(specialization) 的機制。 人類以兩種角度來抽象化事物:「部份」(part-of) 和「種類」(kind-of)。福特汽 車“是一種”(is-a-kind-of-a) 車子,福特汽車“有”(has-a) 引擎、輪胎……等 等零件。「部份」的層次隨著 ADT 的流行,已成為軟體系統的一份子了;而「繼承 」則添入了“另一個”重要的軟體分解角度。 ======================================== Q52:怎樣在 C 中表現出繼承? 用 ": public" 語法: class Car : public Vehicle { //^^^^^^^^---- ": public" 讀作「是一種」("is-a-kind-of-a") //... }; 我們以幾種方式來描述上面的關係: * Car 是「一種」("a kind of a") Vehicle * Car 乃「衍生自」("derived from") Vehicle * Car 是個「特異化的」("a specialized") Vehicle * Car 是 Vehicle 的「子類別」("subclass") * Vehicle 是 Car 的「基底類別」("base class") * Vehicle 是 Car 的「父類別」("superclass") (這不是 C 界常用的說法) 【譯註】"superclass" 是 Smalltalk 語言的關鍵字。 ======================================== Q53:把衍生類別的指標轉型成指向它的基底,可以嗎? 可以。 衍生類別是該基底類別的特異化版本(衍生者「是一種」("a-kind-of") 基底)。這 種向上的轉換是絕對安全的,而且常常會發生(如果我指向一個汽車 Car,實際上我 是指向一個車子 Vehicle): void f(Vehicle* v); void g(Car* c) { f(c); } //絕對很安全;不需要轉型 注意:在這裡我們假設的是 "public" 的繼承;後面會再提到「另一種」"private/ protected" 的繼承。 ======================================== 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:衍生類別的陣列「不是」基底的陣列,是否表示陣列不好? 沒錯,「陣列很爛」(開玩笑的 :-) 。 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[]"),這問題就可以在編譯期被挑出來,而非在執行的時候。 ========================== ● 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 cline@sun.soe.clarkson.edu / 315-353-6100 / FAX: 315-353-6110
------
**********************************************************
哈哈&兵燹
最會的2大絕招 這個不會與那個也不會 哈哈哈 粉好

Delphi K.Top的K.Top分兩個字解釋Top代表尖端的意思,希望本討論區能提供Delphi的尖端新知
K.表Knowlege 知識,就是本站的標語:Open our mind
系統時間:2024-04-29 14:09:54
聯絡我們 | Delphi K.Top討論版
本站聲明
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。
2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。
3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇!