友元函式
分清成員函式
成員函式和非成員函式最大的區別在於成員函式可以是虛擬的而非成員函式不行。所以,如果有個函式必須進行動態綁定(見條款38),就要採用虛擬函式,而虛擬函式必定是某個類的成員函式。關於這一點就這么簡單。如果函式不必是虛擬的,情況就稍微複雜一點。()
舉例
看下面表示有理數的一個類:
class rational {
public:
rational(int numerator = 0,int denominator = 1);
int numerator() const;
int denominator() const;
private:
...
};
這是一個沒有一點用處的類。(用條款18的術語來說,接口的確最小,但遠不夠完整。)所以,要對它增加加,減,乘等算術操作支持,但是,該用成員函式還是非成員函式,或者,非成員的友元函式來實現呢?
當拿不定主意的時候,用面向對象的方法來考慮!有理數的乘法是和rational類相聯繫的,所以,寫一個成員函式把這個操作包到類中。
class rational {
public:
...
const rational operator*(const rational& rhs) const;
};
(如果你不明白為什麼這個函式以這種方式聲明——返回一個const值而取一個const的引用作為它的參數——參考條款21-23。)
條款21: 儘可能使用const
條款22: 儘量用“傳引用”而不用“傳值”
條款23: 必須返回一個對象時不要試圖返回一個引用
可以很容易地對有理數進行乘法操作:
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*⑵; // 運行良好
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,所以才有上面成功的調用(見條款m19)。換句話說,編譯器處理這個調用時的情形類似下面這樣:
const rational temp⑵; // 從2產生一個臨時
// rational對象
result = onehalf * temp; // 同onehalf.operator*(temp);
當然,只有所涉及的構造函式沒有聲明為explicit的情況下才會這樣,因為explicit構造函式不能用於隱式轉換,這正是explicit的含義。如果rational象下面這樣定義:
class rational {
public:
explicit rational(int numerator = 0,// 此構造函式為
int denominator = 1); // explicit
...
const rational operator*(const rational& rhs) const;
...
};
那么,下面的語句都不能通過編譯:
result = onehalf * 2; // 錯誤!
result = 2 * onehalf; // 錯誤!
這不會為混合運算提供支持,但至少兩條語句的行為一致了。
例子結論
然而,我們剛才研究的這個類是要設計成可以允許固定類型到rational的隱式轉換的——這就是為什麼rational的構造函式沒有聲明為explicit的原因。這樣,編譯器將執行必要的隱式轉換使上面result的第一個賦值語句通過編譯。實際上,如果需要的話,編譯器會對每個函式的每個參數執行這種隱式類型轉換。但它只對函式參數表中列出的參數進行轉換,決不會對成員函式所在的對象(即,成員函式中的*this指針所對應的對象)進行轉換。這就是為什麼這個語句可以工作:
result = onehalf.operator*⑵; // converts int -> rational
而這個語句不行:
result = 2.operator*(onehalf); // 不會轉換
// int -> rational
第一種情形操作的是列在函式聲明中的一個參數,而第二種情形不是。
儘管如此,你可能還是想支持混合型的算術操作,而實現的方法應該清楚了:使operator*成為一個非成員函式,從而允許編譯器對所有的參數執行隱式類型轉換:
class rational {
... // contains no operator*
};
// 在全局或某一名字空間聲明,
// 參見條款m20了解為什麼要這么做
const rational operator*(const rational& lhs,
const rational& rhs)
{
return rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
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 {
public:
string(const char *value);
...
istream& operator>>(istream& input);
ostream& operator<<(ostream& output);
private:
char *data;
};
string s;
s >> cin; // 合法,但
// 有違常規
s << cout; // 同上
這會把別人弄糊塗。所以這些函式不能是成員函式。注意這種情況和前面的不同。這裡的目標是自然的調用語法,前面關心的是隱式類型轉換。
正確用法
istream& operator>>(istream& input,string& string)
{
delete [] string.data;
read from input into some memory,and make string.data
point to it
return input;
}
ostream& operator<<(ostream& output,
const string& string)
{
return output << string.data;
}
注意上面兩個函式都要訪問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的成員函式。
友元函式
形式
friend 類型名 友元函式名(形參表);
然後在類體外對友元函式進行定義,定義的格式和普通函式相同,但可以通過對象作為參數直接訪問對象的私有成員
說明如下
:
1)必須在類的說明中說明友元函式,說明時以關鍵字friend開頭,後跟友元函式的函式原型,友元函式的說明可以出現在類的任何地方,包括在private和public部分;
2)注意友元函式不是類的成員函式,所以友元函式的實現和普通函式一樣,在實現時不用"::"指示屬於哪個類,只有成員函式才使用"::"作用域符號;
3)友元函式不能直接訪問類的成員,只能訪問對象成員,
4)友元函式可以訪問對象的私有成員,但普通函式不行;
5)調用友元函式時,在實際參數中需要指出要訪問的對象,
6)類與類之間的友元關係不能繼承。
7)一個類的成員函式也可以作為另一個類的友元,但必須先定義這個類。