C++語言常見問題解:#16 ~ #32

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

C++語言常見問題解
Q16:行內函數是做什麼的?

行內函數(inline function)是個程序代碼會塞入呼叫者所在之處的函數。就像宏
一樣,行內函數免除了函數呼叫的額外負擔,以增進效率,並且(尤其是!)還能讓
編譯器對它施以最佳化(程序融合 "procedural integration")。不過和宏不同
的是:它只會對所有自變量求一次的值(在語意上,該“函數呼叫”和正常函數一樣,
只是比較快速罷了),以避免某些不易察覺的宏錯誤。此外,它還會檢測自變量的型
態,做必要的型別轉換(宏對你有害;除非絕對必要,否則別再用它了)。

注意:過度使用行內函數會讓程序代碼肥胖,於分頁(paging)環境下反而有負面的性
能影響。

宣告法:在函數定義處使用 "inline" 關鍵詞:

inline void f(int i, char c) { /*...*/ }

或者是在類別內將定義包括進去:

class Fred {
public:
void f(int i, char c) { /*...*/ }
};

或是在類別外頭,以 "inline" 來定義該成員函數:

class Fred {
   public:
void f(int i, char c);
};

inline void Fred::f(int i, char c) { /*...*/ }


=============================
■□ 第5節:建構子和解構子
=============================

Q17:建構子(constructor)是做什麼的?

建構子乃用來從零開始建立對象。

建構子就像個「初始化函數」;它把一堆散亂的字節成一個活生生的對象。最低限
度它會初始化內部用到的字段元,也可能會配置所須的資源(內存、檔案、semaphore
、socket 等等)。

"ctor" 是建構子 constructor 最常見的縮寫。

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

Q18:怎樣才能讓建構子呼叫另一個同處一室的建構子?

沒有辦法。

原因是:如果你呼叫另一個建構子,編譯器會初始化一個暫時的區域性對象;但並沒
有初始化“這個”你想要的對象。你可以用預設參數(default parameter),將兩
個建構子合併起來,或是在私有的 "init()" 成員函數中共享它們的程序代碼。

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

Q19:解構子(destructor)是做什麼的?

解構子乃對象之葬禮。

解構子是用來釋放該對象所配置到的資源,譬如:Lock 類別可能會鎖住一個
semaphore,解構子則用來釋放它。最常見的例子是:當建構子用了 "new" 以後,解
構子用 "delete"。

解構子是個「去死吧」的運作行爲(method),通常縮寫爲 "dtor"。


=========================
■□ 第6節:運操作數多載
=========================

Q20:運操作數多載(operator overloading)是做什麼的?

它可讓使用類別的人以直覺來操作之。

運操作數多載讓 C/C++ 的運操作數,能對自訂的型態(對象類別)賦予自訂的意義。它
們形同是函數呼叫的語法糖衣 (syntactic sugar):

class Fred {
public:
//...
};

#if 0
Fred add(Fred, Fred); //沒有運操作數多載
Fred mul(Fred, Fred);
#else
Fred operator+(Fred, Fred); //有運操作數多載
Fred operator*(Fred, Fred);
#endif

Fred f(Fred a, Fred b, Fred c)
{
#if 0
return add(add(mul(a,b), mul(b,c)), mul(c,a)); //沒有...
#else
return a*b + b*c + c*a;             //有...
#endif
}

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

Q21:哪些運操作數可以/不能被多載?

大部份都可以被多載。
不能的 C 運操作數有 "." 和 "?:"(和以技術上來說,可算是運操作數的 "sizeof")。
C++ 增加了些自己的運操作數,其中除了 "::" 和 ".*". 之外都可以被多載。

底下是個足標(subscript)運操作數的例子(它會傳回一個參考)。最前面是“不用
”多載的:

class Array {
public:
#if 0
int& elem(unsigned i) { if (i>99) error(); return data[i]; }
  #else
int& operator[] (unsigned i) { if (i>99) error(); return data[i]; }
#endif
  private:
int data[100];
};

main()
{
Array a;

#if 0
a.elem(10) = 42;
a.elem(12) += a.elem(13);
#else
a[10] = 42;
a[12] += a[13];
#endif
}

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

Q22:怎樣做一個 "**"「次方」運操作數?

無解。

運操作數的名稱、優先序、結合律以及元數(arity)都被語言所定死了。C++ 裏沒有
"**" 運操作數,所以你無法替類別訂做一個它。

還懷疑的話,考慮看看 "x ** y" 和 "x * (*y)",這兩者是完全一樣的(換句話說
,編譯器會假設 "y" 是個指針)。此外,運操作數多載只是函數呼叫的語法糖衣而已
,雖然甜甜的,但本質上並未增加什麼東西。我建議你多載 "pow(base,exponent)"
這個函數(它的倍精確度版本在 <math.h> 中)。

附帶一提:operator^ 可以用,但它的優先序及結合律不符「次方」所需。


===================
■□ 第7節:夥伴
===================

Q23:夥伴(friend)是什麼?

讓別的類別或函數能存取到你的類別內部的東西。

夥伴可以是函數或其它類別。類別會對它的夥伴開放存取權限。正常情況下,程序員
會下意識﹑技術性地控制該類別的夥伴與運作行爲(否則當你想更動類別時,還得先
有其它部份的擁有者之同意才行)。

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

Q24:「夥伴」違反了封裝性嗎?

若善用之,反而會「強化」封裝性。

我們經常得將一個類別切成兩半,當這兩半各有不同的案例個數及生命期時。在此情
形之下,它們通常需要直接存取對方的內部(這兩半“本來”是在同一個類別裏面,
所以你並未“增加”存取數據結構的運作行爲個數;你只是在“搬動”這些運作行爲
所在之處而已)。最安全的實作方式,就是讓這兩半互爲彼此的「夥伴」。

若你如上述般的使用夥伴,你依然是將私有的東西保持在私有的狀態。遇到上述的情
況,如果還呆呆的想避免使用夥伴關係,許多人不是採用公共資料(糟透了!),就
是弄個公共的 get/set 存取運作行爲來存取彼此的資料,事實上這些都破壞了封裝
性。只有在類別的外面該私有資料「仍有其意義」(以使用者的角度來看)時,開放
出私有資料的存取運作行爲才稱得上是恰當的做法。多數情況下,「存取運作行爲」
就和「公共資料」一樣糟糕:它們對私有資料成員只隱其“名”而已,卻未隱藏其“
存在”。

同樣的,如果將「夥伴函數」做爲另一種類別公共存取函數的語法,那就和違反封裝
性的成員函數一樣破壞了封裝。換句話說,對象類別的夥伴及成員都是「封裝的界線
」,如同「類別定義」本身一樣。

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

Q25:夥伴函數的優缺點?

它提供了某種接口設計上的自由。

成員函數和夥伴函數都有同等的存取特權(100% 的權利),主要的差別在於:夥伴
函數用起來像是 "f(x)",而成員函數則是 "x.f()"。因此,夥伴函數可讓對象類別
設計者挑選他看得最順眼的語法,以降低維護成本。

夥伴函數主要的缺點在於:當你想做動態繫結(dynamic binding)時,它需要額外
的程序代碼。想做出「虛擬夥伴」的效果,該夥伴函數應該呼叫個隱藏的(通常是放在
"protected:" 裏)虛擬成員函數;像這個樣子:"void f(Base& b) { b.do_f(); }"
。衍生類別會覆蓋(override)掉那個隱藏的成員函數("void Derived::do_f()")
,而不是該夥伴函數。

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

Q26:「夥伴關係無繼承及遞移性」是什麼意思?

夥伴關係的特權性無法被繼承下來:夥伴的衍生類別不必然還是夥伴(我把你當朋友
,但這不代表我也一定會信任你的孩子)。如果 "Base" 類別宣告了 "f()" 爲它的
夥伴,"f()" 並不會自動對由 "Base" 衍生出來的 "Derived" 類別所多出來的部份
擁有特殊的存取權力。

夥伴關係的特權無遞移性:夥伴類別的夥伴不必然還是原類別的夥伴(朋友的朋友不
一定也是朋友)。譬如,如果 "Fred" 類別宣告了 "Wilma" 類別爲它的夥伴,而且
"Wilma" 類別宣告了 "f()" 爲它的夥伴,則 "f()" 不見得對 "Fred" 有特殊的存取
權力。

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

Q27:應該替類別宣告個成員函數,還是夥伴函數?

可能的話,用成員函數;必要時,就用夥伴。

有時在語法上來看,夥伴比較好(譬如:在 "Fred" 類別中,夥伴函數可把 "Fred"
弄成是第二個參數,但在成員函數中則一定得放在第一個)。另一個好例子是:二元
中序式算數運操作數(譬如:"aComplex + aComplex" 可能應該定義成夥伴而非成員函
數,因爲你想讓 "aFloat + aComplex" 這種寫法也能成立;回想一下:成員函數無
法提升它左側的參數,因爲那樣會把引發該成員函數的對象所屬之類別給改變掉)。

在其它情況下,請選成員函數而不要用夥伴函數。


====================================================
■□ 第8節:輸入/輸出:<iostream.h> 和 <stdio.h>
====================================================

Q28:該怎樣替 "class Fred" 提供輸出功能?

用夥伴函數 operator<<:

class Fred {
public:
friend ostream& operator<< (ostream& o, const Fred& fred)
{ return o << fred.i; }
//...
 private:
int i; //只爲了說明起見而設的
};

我們用夥伴而不用成員函數,因爲 "Fred" 是第二個參數而非第一個。輸入的功能亦
類似,只是要改寫成:

istream& operator>> (istream& i, Fred& fred);
// ^^^^^------- 不是 "const Fred& fred"!

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

Q29:爲什麼我該用 <iostream.h> 而不是以前的 <stdio.h>?

增加型別安全、減少錯誤、增進效率、有延展性、提供衍生能力。

Printf 還好,而 scanf 除了容易寫錯之外也還算可以,然而和 C++ 的 I/O 系統相
比,它們都有其限制。C++ 的 I/O(用 "<<" 及 ">>" ),和 C( "printf()" 和
"scanf()" )相比:

* 型別安全--要做 I/O 的對象,編譯器會靜態地事先得知其型別,而不是動態地
由 "%" 一欄查知。

* 不易出錯--冗餘的信息會增加錯誤的機會。C++ 的 I/O 就不需要多餘的 "%"。

* 更快速--printf 是個小型語言的「解譯器」,該語言主要是由 "%" 這種東西
構成的;在執行期它用這些字段來選擇正確的格式化方式。C++ 的 I/O 系統則是
靜態的依各自變量真正的型別來挑選子程序,以增進執行效率。

* 延展性--C++ I/O 機制可在不改動原有程序代碼的情況下,就加進使用者新設計
的型態(能想象如果大家同時把互不兼容的 "%" 字段塞入 printf 和 scanf,會
是怎樣的混亂場面?!)。

* 可衍生(subclassable)--ostream 和 istream(C++ 的 FILE* 代替品)都是
真正的類別,因此可以被衍生下去。這意味着:你可以讓其它自定的東西有着和
stream 雷同的外表與行爲,但實際上做的卻是你想做的特定事情。你自動就重用
了數以萬計別人(你甚至不認識它們)寫好的 I/O 程序代碼,而他們也不需要知道
你所做的「延伸 stream」類別。

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

Q30:爲什麼我處理輸入時,會超過檔案的結尾?

因爲 eof(檔案結尾)的狀態,是到「將要超過檔案結尾的動作」纔會被設定。也就
是說,讀檔案的最後一個字節並不會設定 eof 的狀態。

【譯註】這也是 C 常見的錯誤。

如果你的程序像這樣:

int i = 0;
while (! cin.eof()) {
cin >> x;
++i;
// work with x
}

你的 i 變量就會多了一。
你真正該做的是這樣:

int i;
while (cin >> x) {
++i;
// work with x
}

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

Q31:爲什麼我的程序執行完第一次循環後,會對輸入的要求不加理睬?

因爲讀取數值的程序,把非數字的字符留在輸入緩衝區 (input buffer) 裏頭了。

【譯註】這也是 C,甚至 Pascal 常見的錯誤。

如果你的程序如下:

char name[1000];
int age;

for (;Wink {
cout << "Name: ";
cin >> name;
cout << "Age: ";
cin >> age;
}

你應該這樣寫:

for (;Wink {
cout << "Name: ";
cin >> name;
cout << "Age: ";
cin >> age;
cin.ignore(INT_MAX, '/n');
}

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

Q32:在 DOS 及 OS/2 的 binary 模式下,要怎樣來 "reopen" cin 及 cout?

有這個問題,最典型的情況就是:有人想對 cin、cout 做 binary 的 I/O,但是作
業系統(像是 DOS 或 OS/2)卻總是會做 CR-LF 的轉換動作。

解決法:cin、cout、cerr 這些事先定義好的串流,都是 text 的串流,沒有標準做
法能把它們弄成 binary 模式。把串流關掉再設法以 binary 模式 reopen 它們,可
能會導致不可預期的結果。

在這兩種模式有不同行爲的系統上,一定有辦法讓它們變成 binary 串流,但是你得
去查查該系統的文件。

--
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  


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