友元函數
友元函數
友元函數是指某些雖然不是“類”成員卻能夠訪問類的所有成員的函數。類授予它的友元特別的訪問權。通常同一個開發者會出於技術和非技術的原因,控制類的友元和成員函數(否則當你想更新你的類時,還要徵得其它部分的擁有者的同意)。
成員函數和非成員函數最大的區別在於成員函數可以是虛擬的而非成員函數不行。所以,如果有個函數必須進行動態綁定(見條款38),就要採用虛擬函數,而虛擬函數必定是某個類的成員函數。關於這一點就這麼簡單。如果函數不必是虛擬的,情況就稍微複雜一點。()
看下面表示有理數的一個類:
class rational {
public:
rational(int numerator=0,int denominator=1);
int numerator()const;
int denominator()const;
...
};
這是一個沒有一點用處的類。(用條款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象下面這樣定義:
classrational{
public:
explicitrational(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成員,而這個成員是私有(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)一個類的成員函數也可以作為另一個類的友元,但必須先定義這個類。
目錄