0.前言
什麼是友元?友元是允許另一個類或者函數訪問某個類非public成員的機制,方法是使用friend說明符在類定義中進行額外的聲明。
既然友元可以訪問類的非公有成員,那麼可以認爲在一定程度上破壞了類的封裝性。但我們也可以把他們看成一個整體,那麼友元也就是封裝的一部分。並且,友元使得編碼更加自由,提高了靈活性。
和普通的類成員不同,友元關係既不繼承,也不傳遞。友元不一定是派生類的友元,這種關係不被繼承;友元的友元也不一定是友元;此外,友元聲明是單向的,若類A聲明瞭友元類B,而B沒有聲明友元A,則只有B可以訪問A的非公有成員。
一般,我們有三種友元的使用方式:友元函數、友元類、友元成員函數,下面我們進一步學習。
1.友元函數
一個常規的成員函數聲明描述了三件在邏輯上相互不同的事情:
- 該函數能訪問類聲明的非公有部分;
- 該函數位於類的作用域中;
- 該函數必須經由一個對象去激活(有一個this指針)。
將函數聲明爲static,可以讓他只具有前兩種性質。將函數聲明爲friend友元,可以讓他只具有第一種性質。
下面我們通過一個例子來演示友元函數的使用,自定義一個Number類,通過友元函數add、sub來進行加減(暫不涉及操作符重載的知識,所以直接用函數)。代碼如下:
#include <iostream>
//自定義一個數值類型,用於測試友元
class Number
{
public:
//通過一個浮點數進行構造
explicit Number(double val) :_value(val) {}
//通過公有接口獲取值
double value() const { return _value; }
private:
double _value;
//聲明加上friend,即爲友元
//定義可以在外部或內部,但是需要在外部做聲明以便全局訪問該函數
friend Number add(const Number& a, const Number& b)
{
//聲明爲友元,就可以直接訪問類的非公有成員
return Number(a._value + b._value);
}
friend Number sub(const Number& a, const Number& b);
};
//函數的聲明,定義可以放cpp中
Number add(const Number& a, const Number& b);
Number sub(const Number& a, const Number& b)
{
return Number(a._value - b._value);
}
int main()
{
Number a(10.5), b(0.5);
Number c = add(a, b); //=11
Number d = sub(a, b); //=10
std::cout << "a:" << a.value() << "\t"
<< "b:" << b.value() << "\n"
<< "a+b=c:" << c.value() << "\n"
<< "a-b=d:" << d.value() << std::endl;
system("pause");
return 0;
}
輸出結果:
友元的聲明僅僅指定了訪問權限,而非一個通常意義上的函數聲明。如果我們希望能夠調用某個友元函數,那麼就必須在友元聲明之外專門對函數進行一次聲明。爲了使友元對類的用戶可見,一般把友元聲明和類本身放在同一個頭文件中(類的外部,獨立進行聲明)。
類和非成員函數的聲明不是必須在他們的友元聲明之前。當一個名字第一次出現在一個友元聲明中時,我們隱式地假定該名字在當前作用域中是可見的。然而,友元本身不一定真的聲明在當前的作用域中。甚至就算在類內部定義該函數,我們也必須在類地外部提供相應地聲明從而是的函數可見。
2.友元類
友元類的應用,我覺得C++PrimerPlus的例子挺好,TV和Remote遙控器兩個類,很明顯 is-a 和 has-a 兩種關係都不成立。通過在TV中把Remote聲明爲友元類,這樣就可以用Remote遙控器來對TV進行設置。
這裏我直接擴展上一節的例子,再定義一個Fraction分數類,在Number數值類中將分數類聲明爲友元,這樣就可以直接訪問他的值而不用通過公有接口。代碼如下:
#include <iostream>
class Fraction;
//自定義一個數值類型,用於測試友元函數
class Number
{
public:
//通過一個浮點數進行構造
explicit Number(double val = 0.0) :_value(val) {}
//通過公有接口獲取值
double value() const { return _value; }
private:
double _value;
//聲明加上friend,即爲友元
//定義可以在外部或內部,但是需要在外部做聲明以便全局訪問該函數
friend Number add(const Number& a, const Number& b)
{
//聲明爲友元,就可以直接訪問類的非公有成員
return Number(a._value + b._value);
}
friend Number sub(const Number& a, const Number& b);
friend class Fraction;
};
//函數的聲明,定義可以放cpp中
Number add(const Number& a, const Number& b);
Number sub(const Number& a, const Number& b)
{
return Number(a._value - b._value);
}
//自定義一個分數類型,配合Number類測試友元類
class Fraction
{
public:
//通過分子分母構造一個分數
Fraction(const Number& numerator, const Number& denominator)
:_numerator(numerator), _denominator(denominator) {}
//通過公有接口獲取分數的浮點值
double value() const
{
//可以通過Number的公有接口訪問其值
//return _numerator.value()/_denominator.value();
//也可以把Fraction聲明爲Number的友元,然後直接訪問其值
return _numerator._value / _denominator._value;
}
private:
//分子
Number _numerator;
//分母
Number _denominator;
};
int main()
{
Number a(1.5), b(0.5);
Fraction c(a, b);
std::cout << "a:" << a.value() << "\t"
<< "b:" << b.value() << "\n"
<< "a/b=c:" << c.value() << std::endl;
system("pause");
return 0;
}
輸出結果:
(在C++Qt框架源碼中也大量使用了友元類。)
3.友元成員函數
在使用友元類的時候,可能只有部分方法涉及到了非公有操作,我們可以只把這些成員函數作爲友元(如TV只把遙控器設置頻道的函數聲明爲友元)。
我們需要仔細組織程序的結構以滿足聲明和定義的批次依賴關係,假設有類TV和類Remote,定義在同一個文件,我們需要這樣組織代碼:
- 聲明TV類;
- 定義Remote類,聲明addChannel成員函數(先不定義);
- 定義TV類,將Remote::addChannel函數聲明爲友元;
- 定義Remote::addChannel成員函數,此時可以在該函數中訪問TV的私有成員。
#include <iostream>
class TV;
class Remote
{
public:
void addChannel(TV& tv);
};
class TV
{
friend void Remote::addChannel(TV& tv);
public:
explicit TV(int channel = 0) :_channel(channel) {}
int channel() const { return _channel; }
private:
int _channel;
};
void Remote::addChannel(TV& tv) {
tv._channel += 1;
}
int main()
{
TV tv(0);
Remote remote;
std::cout << "current channel:" << tv.channel() << std::endl;
remote.addChannel(tv);
std::cout << "current channel:" << tv.channel() << std::endl;
system("pause");
return 0;
}
輸出結果:
4.其他
在運算符重載時,我們可以將函數聲明爲友元,而不是作爲成員函數。
- C++規定,賦值運算符“=”、下標運算符“[]”、函數調用運算符“()”、成員運算符“->”必須作爲成員函數。
- 流插入“<<”和流提取運算符“>>”、類型轉換運算符不能定義爲類的成員函數,只能作爲友元函數。
- 一般將單目運算符和複合運算符(+=,-=,/=,*=,&=,!=,^=,%=,>>=,<<=)重載爲成員函數。
- 一般將雙目運算符重載爲友元函數。
以輸出流【<<】爲例:
#include <iostream>
class Number
{
public:
//通過一個浮點數進行構造
explicit Number(double val=0.0) :_value(val) {}
//通過公有接口獲取值
double value() const { return _value; }
private:
double _value;
//通過運算符<<獲取值
friend std::ostream& operator<<(std::ostream& output, Number& n)
{
output << n._value;
return output;
}
};
5.參考
參考書籍:《C++Primer》中文第五版
參考書籍:《C++PrimerPlus》中文第六版
參考書籍:《C++程序設計語言》十週年中文紀念版
參考文檔:https://zh.cppreference.com/w/cpp/language/friend
參考博客:https://blog.csdn.net/ljq550000/article/details/6061535