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

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