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