C++學習筆記:友元

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

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