分清成員函數,非成員函數和友元函數

成員函數和非成員函數最大的區別在於成員函數可以是虛擬的而非成員函數不行。所以,如果有個函數必須進行動態綁定(見條款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。)

現在可以很容易地對有理數進行乘法操作:

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,所以纔有上面成功的調用(見條款m19)。換句話說,編譯器處理這個調用時的情形類似下面這樣:

const rational temp(2);      // 從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*(2);      // 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的成員函數。

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