成員函式和非成員函式最大的區別在於成員函式可以是虛擬的而非成員函式不行。所以,如果有個函式必須進行動態繫結,就要採用虛函式,而虛函式必定是某個類的成員函式。如果函式不必是虛擬的,情況就稍微複雜一點。
看下面表示有理數的乙個類:
class rational ;
這是乙個沒有一點用處的類(介面最小,但遠不夠完整)。所以,要對它增加加,減,乘,除等算術操作支援,但是,該用成員函式還是非成員函式,或者,非成員的友元函式來實現呢?
當拿不定主意時,用物件導向的方法來考慮。有理數的乘法是和rational類相聯絡的,所以,寫乙個成員函式把這個操作包到類中。
class rational ;
現在可以很容易地對有理數進行乘法操作:
rational oneeighth(1, 8);
rational onehalf(1, 2);
rational result = onehalf * oneeighth; // 執行良好
result = result * oneeighth; // 執行良好
還要支援混合型別操作,比如,rational要能和int相乘。但當寫下下面的**時,只有一半工作:
result = onehalf * 2; // 執行良好
result = 2 * onehalf; // 出錯!
如果用下面的等價函式形式重寫上面的兩個例子,問題的原因就很明顯了:
result = onehalf.operator*(2); // 執行良好
result = 2.operator*(onehalf); // 出錯!
物件onehalf是乙個包含operator*函式的類的例項,所以編譯器呼叫了那個函式。而整數2沒有相應的類,所以沒有operator*成員函式。編譯器還會去搜尋乙個可以象下面這樣呼叫的非成員的operator*函式(即在某個可見的名字空間裡的operator*函式或全域性的operator*函式):
result = operator*(2, onehalf); // 錯誤!
但沒有這樣乙個引數為int和rational的非成員operator*函式,所以搜尋失敗。
再看看那個成功的呼叫。它的第二引數是整數2,然而rational::operator*期望的引數卻是rational物件。為什麼2在乙個地方可以工作而另乙個地方不行?
秘密在於隱式型別轉換。編譯器知道傳的值是int而函式需要的是rational,但它也同時知道呼叫rational的建構函式將int轉換成乙個合適的rational,所以才有上面成功的呼叫。換句話說,編譯器處理這個呼叫時的情形類似下面這樣:
const rational temp(2); // 從2產生乙個臨時rational物件
result = onehalf * temp; // 同onehalf.operator*(temp);
只有所涉及的建構函式沒有宣告為explicit的情況下才會這樣,因為explicit建構函式不能用於隱式轉換,這正是explicit的含義。如果rational象下面這樣定義:
class rational ;
那麼,下面的語句都不能通過編譯:
result = onehalf * 2; // 錯誤!
result = 2 * onehalf; // 錯誤!
這不會為混合運算提供支援,但至少兩條語句的行為一致了。
這個類設計成可以允許固定型別到rational的隱式轉換的——這就是為什麼rational的建構函式沒有宣告為explicit的原因。這樣,編譯器將執行必要的隱式轉換使上面result的第乙個賦值語句通過編譯。實際上,如果需要的話,編譯器會對每個函式的每個引數執行這種隱式型別轉換。但它只對函式引數表中列出的引數進行轉換,決不會對成員函式所在的物件(即,成員函式中的*this指標所對應的物件)進行轉換。這就是為什麼這個語句可以工作:
result = onehalf.operator*(2); // converts int -> rational
而這個語句不行:
result = 2.operator*(onehalf); // 不會轉換 int -> rational
第一種情形操作的是列在函式宣告中的乙個引數,而第二種情形不是。儘管如此,還想支援混合型的算術操作,而實現的方法應該清楚了:使operator*成為乙個非成員函式,從而允許編譯器對所有的引數執行隱式型別轉換:
class rational ;
// 在全域性或某一名字空間宣告
const rational operator*(const rational& lhs,
const rational& rhs)
rational onefourth(1, 4);
rational result;
result = onefourth * 2; // 工作良好
result = 2 * onefourth; // 它也工作了!
這當然是乙個完美的結局,但還有乙個擔心:operator*應該成為rational類的友元嗎?
這種情況下,答案是不必要。因為operator*可以完全通過類的公有(public)介面來實現。上面的**就是這麼做的。只要能避免使用友元函式就要避免,因為,和現實生活中差不多,友元帶來的麻煩往往比它產生的幫助多。
然而,很多情況下,不是成員的函式從概念上說也可能是類介面的一部分,它們需要訪問類的非公有成員的情況也不少。
string類,若想過載operator>>和operator《來讀寫string物件,發現它們不能是成員函式。如果是成員函式的話,呼叫它們時就必須把string物件放在它們的左邊:
// 乙個不正確地將operator>>和operator《作為成員函式的類
class string ;
string s;
s >> cin; // 合法, 但有違常規
s << cout; // 同上
這會把別人弄糊塗。所以這些函式不能是成員函式。注意這種情況和前面的不同。這裡的目標是自然的呼叫語法,前面關心的是隱式型別轉換。
所以,如果來設計這些函式,就象這樣:
istream& operator>>(istream& input, string& string)
ostream& operator<<(ostream& output,
const string& string)
注意上面兩個函式都要訪問string類的data成員,而這個成員是私有(private)的。已經知道,這個函式一定要是非成員函式。這樣,就別無選擇了:需要訪問非公有成員的非成員函式只能是類的友元函式。
本條款得出的結論如下。假設f是想正確宣告的函式,c是和它相關的類:
·虛函式必須是成員函式。如果f必須是虛函式,就讓它成為c的成員函式。
·operator>>和operator《決不能是成員函式。如果f是operator>>或operator<<,讓f成為非成員函式。如果f還需要訪問c的非公有成員,讓f成為c的友元函式。
·只有非成員函式對最左邊的引數進行型別轉換。如果f需要對最左邊的引數進行型別轉換,讓f成為非成員函式。如果f還需要訪問c的非公有成員,讓f成為c的友元函式。
·其它情況下都宣告為成員函式。如果以上情況都不是,讓f成為c的成員函式。
分清成員函式,非成員函式和友元函式
成員函式和非成員函式最大的區別在於成員函式可以是虛擬的而非成員函式不行。所以,如果有個函式必須進行動態繫結 見條款38 就要採用虛函式,而虛函式必定是某個類的成員函式。關於這一點就這麼簡單。如果函式不必是虛擬的,情況就稍微複雜一點。看下面表示有理數的乙個類 class rational 這是乙個沒有...
分清成員函式,非成員函式和友元函式
成員函式和非成員函式最大的區別在於成員函式可以是虛擬的而非成員函式不行。所以,如果有個函式必須進行動態繫結 見條款38 就要採用虛函式,而虛函式必定是某個類的成員函式。關於這一點就這麼簡單。如果函式不必是虛擬的,情況就稍微複雜一點。看下面表示有理數的乙個類 class rational 這是乙個沒有...
C 友元一 成員函式 非成員函式和友元函式
類有成員變數和成員函式。而函式中,不是任何乙個類的成員的函式,就是非成員函式。例如 class a 這個就是成員函式。void f2 這個也是成員函式宣告,其實現在類的外部。void a f2 這個是成員函式的實現。void f3 這個就是非成員函式,它不屬於a,也不屬於任何一起其他的類。成員函式和...