【例1】
将数据与处理数据的函数封装在一起,构成类,既实现了数据的共享又实现了隐藏,无疑是面向对象程序设计的一大优点。但是封装并不总是绝对的。现在考虑一个简单的例子,就是Point类,每一个Point类的对象代表一个“点”。如果需要一个函数来计算任意两点之间的距离,这个函数该怎样设计呢?
如果将计算距离的函数设计为类外的普通函数,就不能体现这个函数与“点”之间的联系,而且类外的函数也不能之间引用“点”的坐标(私有成员),这样计算很不方便。
那么设计Point类的成员函数应该怎样设计呢?从语法的角度这不难实现,但是不好理解。因为距离是点与点之间的一种关系,它既不属于每一个单独的点,也不属于整个Point类。也就是说无论把距离函数设计为非静态成员还是静态成员都会影响程序的可读性。
之前在类的组合中,通过Point的两个对象组合成Line(线段)类,具有计算线段长度的功能,但是Line类的实质是对线段的抽象。如果我们经常需要计算任意两点之间的距离,那么每次计算两点之间距离的时候都要先构造一个线段,这样既麻烦又影响程序的可读性。
这种情况下,需要一个在Point类外,但与Point类有特殊关系的函数。
【例2】
class A
{
public:
void display() { cout << x << endl; }
int getX() { return x; }
private:
int x;
};
class B
{
void set(int i);
void display();
private:
A a;
};
这是组合类的情况,类B中内嵌了类A的对象,但是B的成员函数却无法直接访问A的私有成员x。从数据安全性角度来说,这无疑是最安全的,内嵌的部件相当于一个黑盒。但是使用起来有些不方便,例如,按如下形式实现B的成员函数set,会引起编译错误:
void B::set(int i)
{
a.x = i;
}
由于A的对象内嵌于B中,如何能让B的函数直接访问A的私有数据呢?
C++为上述两个例子中的需求提供了语法支持,就是友元关系。
友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。
通俗的说,友元关系就是一个类主动声明哪些其他类或函数是它的朋友,进而给它们提供对本类的访问特许。也就是说,通过友元关系,一个普通函数或者类的成员函数可以访问封装于另一个类中的数据。从一定程度上将,友元是对数据隐藏和封装的破坏。但是为了数据共享,提高程序的效率和可读性,很多情况下这种小的破坏也是必要的,关键是一个度的问题,要在共享和封装之间找到一个恰当的平衡。。
在一个类中,可以利用关键字 friend
将其他函数或类声明为友元。 如果友元是一般函数或类的成员函数,称为友元函数;如果友元是一个类,则称为友元类,友元类的所有成员函数都自动称为友元函数。
1.友元函数
友元函数是在类中用关键字修饰的非成员函数。友元函数可以是一个普通的函数,也可以是其他类的成员函数。虽然友元函数不是本类的成员函数,但是在友元函数的函数体中可以通过对象名访问类的私有成员和保护成员。
【例】在介绍类的组合时,使用了Point类组合构成的Line类计算线段的长度。现在将采用友元函数来实现更一般的功能:计算任意两点之间的距离。屏幕上的点仍然用Point类来描述,两点之间的距离用普通函数dist来计算。计算过程中,函数dist需要访问Point类的私有数据成员x和y,为此将dist声明为Point类的友元函数。
#include<iostream>
using namespace std;
class Point//Point类的定义
{
public://外部接口
Point(int x = 0,int y=0):x(x),y(y){}
int getX() { return x; }
int getY() { return y; }
friend float dist(Point& p1, Point& p2);//友元函数声明
private://私有数据成员
int x, y;
};
float dist(Point& p1, Point& p2)//友元函数实现
{
double x = p1.x - p2.x;//通过对象访问Point类的私有数据成员
double y = p1.y - p2.y;
return static_cast<float>(sqrt(x * x + y * y));
}
int main()//主函数
{
Point myp1(1, 1), myp2(4, 5);//定义Point类的对象
cout << "两点之间的距离为:";
cout << dist(myp1, myp2) << endl;//计算两点之间的距离
return 0;
}
运行结果及分析:
在Point类中只声明了友元函数的原型,友元函数的定义在类外。可以看到在友元函数中通过使用对象名直接访问了Point类中的私有数据成员x和y,这就是友元关系的关键所在。对于计算任意两点之间的距离这个问题来说,使用友元与使用类的组合相比,可以使程序具有更好的可读性。当然,如果是要表示线段,无疑是使用组合类Line类更为恰当。这就说明对于同一个问题,虽然语法上可以有多个解决方案,但应该根据问题的实质,选择一种比较直接地反映问题域的本来面目的方案,这样程序才会有更高的可读性。
友元函数不仅可以是一个普通函数,也可以是另外一个类中的成员函数。友元成员函数的使用和一般友元函数的使用基本相同,只是要通过相应的类或对象名进行访问。
2.友元成员函数
class Date;//前向引用声明
//类Time的成员函数中使用了Date类的对象,而此时Date类尚未被完整定义
class Time
{
public:
Time(int a=0, int b=0, int c=0) :hour(a), min(b), se(c) {}
void display(Date a);//只有public的成员函数才能成为其它类的友元函数
private:
int hour, min, se;
};
class Date
{
public:
Date(int a, int b, int c) :year(a), month(b), day(c) {}
friend void Time::display(Date a);//将类Time的成员函数display()声明为类Date的友元函数
private:
int year, month, day;
};
void Time::display(Date a)//Time的成员函数display()的实现
{
cout << a.year << "年" << a.month << "月" << a.day << "日 ";
cout << hour <<":"<< min <<":" << se << endl;
}
int main() {
Date d1(2023, 8, 2);
Time t1(13, 12, 23);
t1.display(d1);//将Date类的对象d1作为函数display()的参数,然后用Time类的对象t1访问友元成员函数display
return 0;
}
运行结果及分析:
在程序中display函数是Time类的成员函数,在Date类中将Time类的成员函数display声明为友元成员函数,所以在display函数的实现的时候,将Date类的对象作为display函数的实参,否则无法访问Date类对象的私有数据成员,在display函数体中访问Date类的数据时必须通过对象名去访问例如a.year;
。
【注意】Date类 和Time类的顺序不能改变。因为,只有当一个类的定义已经被看到时,它的成员函数才能被声明为另一个类的友元。
成员函数的声明必须在它的友元成员函数声明之前
如:
X类中的成员函数是Y类中的友元函数:
(1)先定义X类,声明成员函数,不能在声明成员函数的时候定义该函数,要在两个类都定义结束之后再定义实现该成员函数。
(2)再定义Y类,声明X类中的成员函数为该类的友元成员函数。
(3)最后定义X类中的成员函数。
【注意】X类中的成员函数是Y类中的友元函数,则X类中的成员函数可以访问Y类的私有和保护数据成员
【例】
class Object;
class Int
{
private:
int value;
public:
Int(int x = 0) :value(x) {}
~Int() {}
friend void Object::Print( Int& it);//注册为类的友元函数
};
void Object::Print(Int& it)
{
cout << it.value << endl;
}
class Object
{
public:
void Print(Int& it);
};
int main()
{
Int a(10);
Object obj;
obj.Print(a);
return 0;
}
上例中,当一个成员函数还没有在某个具体的类中声明时,就在一开始定义的类中声明了某个类中的成员函数为这个类的友元成员函数,编译则不会通过。改写后如下:
class Int;
class Object
{
public:
void Print(Int& it);
};
class Int
{
private:
int value;
public:
Int(int x = 0) :value(x) {}
friend void Object::Print( Int& it);//注册为类的友元函数
};
void Object::Print(Int& it)
{
cout << it.value << endl;
}
int main()
{
Int a(10);
Object obj;
obj.Print(a);
return 0;
}
运行结果:
【补充】友元函数都没有this指针,所以要用类作为形参来写。
3.友元类
同友元函数一样,一个类可以将另一个类声明为友元类。若A类为B类的友元类,则A类的所有成员函数都是B类的友元函数,都可以访问B类的私有和保护成员。
声明友元类的语法形式如下:
class B
{
...//B类的成员声明
friend class A;//声明A类为B类的友元类
};
声明友元类,时建立类与类之间的联系,实现类与类之间数据共享的一种途径。
【例】B类是A类的友元类,则B类成员函数都为A类的友元函数,所以B类的成员函数可以直接访问A的私有成员。
class A
{
private:
int data;
public:
void display()
{
cout << "data = " << data << endl;
}
friend class B; // 将B类声明是A类的友元类
};
class B
{
public:
void change(int x, A& a)//引用
{
a.data = x;//通过A类的对象a访问A类的私有数据成员data
a.display();通过A类的对象a访问A类的成员函数display
}
};
int main()
{
A a;//A类对象a
B b;//B类对象b
b.change(50, a);//以50和A类对象a作为B类成员函数change的参数,通过B类对象b去调用函数change
return 0;
}
运行结果:
总结
1.友元关系不能传递
B类是A类的友元,C类是B类的友元,C类和A类之间,如果没有声明,就没有任何关系,不能进行数据共享。
2.友元关系是单向的
如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数不可以访问B类的私有和保护数据。
3.友元关系是不被继承的
如果B类是A类的友元,B类的派生类并不会自动成为A类的友元。