由于C++控制了对类对象的访问(例如不允许访问私有成员)。于是,通常公有类方法(例如:成员函数)提供唯一的访问途径。
这样保护了私有成员,但同时又因为这种限制太严格,以致于不适合特定的编程问题。
在这种情况下,C++提供了另外一种形式的访问权限:友元。
友元有三种:
①友元函数;
②友元类;
③友元成员函数。
通过让函数成为类的友元,可以赋予该函数与类的成员函数具有相同的访问权限(例如可以访问、修改私有成员)。
为什么需要友元函数:
以类成员函数为例:
Skill Skill::operator+(const Skill&b)const { Skill another; another.name = name; //名字延续加号前的 another.jilv = (jilv + b.jilv)*1.25; //几率为两个几率之和,乘以1.2 if (another.jilv > 100)another.jilv = 100; //如果几率大于100,则为100 another.dam = dam + b.dam*0.5; //伤害为第一个伤害加上第二个伤害的一半 return another; //返回对象(不是引用) }
这个函数是运算符重载,重载对象是加号。本函数如果修改,有以下可能性:
①当我们面对2个类对象时,使用本函数正常;
②当我们遇见一个类对象,一个基本类型的变量时(例如int a),并且类对象处于加号之前,也简单。将参数换位const int a,然后函数内部代码按正常情况修改即可;
③遇见2个基本类型变量,是无法使用运算符重载的,忽视这种可能;
④遇见一个类对象,一个基本类型变量(例如int a),但此时,基本类型处于加号之前。也就是 对象 + a 变为 a + 对象 这种形式。
按照正常思维,根据加法交换律,这两个相加应该没什么区别。但事实上我们知道,由于运算符重载,在有②号情况函数在的时候,前者可以运行,但后者无法运行(因为没有对应的函数调用)。
这样从逻辑上来讲没什么问题(这里的加号的作用不是面对2个算数值的加号作用),但是这样很不方便。因为这强迫程序员在码代码的时候,必须加号前面是对象,后面是基本类型。换一句话说,不友好。
那么我们在类的成员函数里再加一个运算符重载函数?并不可行。原因在于,类对象位于加号前面时,是作为对象调用运算符重载函数。而一个基本类型位于加号前面时,是无法调用类成员函数的。即对象+a的实质是:
对象.Skill::operator+(const int a)const
我们显然不能书写成:a.Skill::operator+(const Skill&b)const 这种形式,因为a根本不算类对象(他是基本类型)。
但我们的确需要一个运算符重载函数,那么我们写成一般函数?例如:
Skill operator+(const Skill&b)
{
...
}
即参数是skill对象,返回值也是一个skill类对象。
问题来了(1)int a在哪里?在函数定义里如何书写?
(2)因为他不在类成员函数里,那么b.dam这样的调用自然也是不行的(因为只有在成员函数内才能访问私有成员);
对第(1)个问题很简单,把int a加到参数里,第一个参数表示加号前面的数字。例如:Skill operator+(int a,const Skill&b);
对于第二个问题,便只能启用友元函数这个概念了。
友元函数:
①函数原型位于类的公有成员(public)处进行声明;
②函数原型前加friend,函数定义中不加friend;
③函数定义前,不加类的定义域解析运算符(例如Skill::这样的,不加);
④重载的运算符使用哪个友元函数,根据对象的类来决定(例如有两个类,Skill和Player,当调用不同的参数时,编译器会根据运算符重载的几个重载函数,来进行重载函数的参数匹配,寻找到参数类型相符的函数(重载函数的调用,是根据参数的匹配程度来决定的)。
如代码:
#include<iostream> class Skill { int a; public: Skill(int b = 1) { a = b; } int operator+(const int b) { return a + b; } friend int operator+(int a, Skill b); //注意,返回值是a+skill类的私有成员a的和。另外,这里要加friend,但函数定义的时候不加 }; class Player { int a; public: Player(int b = 5) { a = b; }; int operator+(const int b) { return a + b; } friend int operator+(int a, Player b); //注意,返回值是a+player类的私有成员a的和 }; int main() { using namespace std; Skill m; Player n; //这2个的默认构造函数给私有成员赋的值是不一样的,所以最后体现是在使用运算符之后的返回值是不同的 cout << "m + 5 = " << m + 5 << endl; cout << "5 + m = " << 5 + m << endl; cout << "n + 5 = " << n + 5 << endl; cout << "5 + n = " << 5 + n << endl; system("pause"); return 0; } int operator+(int a, Skill b) //注意,这里不加friend { return a + b.a; } int operator+(int a, Player b) { return a + b.a; }
显示:
m + 5 = 6 5 + m = 6 n + 5 = 10 5 + n = 10 请按任意键继续. . .
总结:
①通过使用运算符重载,使得m+5和5+m这样的效果是一样的;
②另外,由于使用了2个友元函数,分别是不同类的友元,于是使用运算符时,会根据类,调用不同的友元函数(而不是调用相同的友元函数);
③如果已经有一个 对象+基本类型 这种形式的运算符重载了,也可以通过一个友元函数,在函数内部,交换参数位置,把 基本类型+对象 变为 对象+基本类型 这样,则可以使用已有的运算符重载。
例如:
#include<iostream> class Skill { int a; public: Skill(int b = 1) { a = b; } int operator+(const int b) { return a + b; } friend int operator+(int a, Skill b); //这里是友元重载函数 }; int main() { using namespace std; Skill m; cout << "m + 5 = " << m + 5 << endl; cout << "5 + m = " << 5 + m << endl; system("pause"); return 0; } int operator+(int a, Skill b) { return b+a; //把a+b转换为b+a,于是b+a调用类方法中的重载运算符了 }
④能不能使用模板类,作为友元函数,不是很清楚,存疑。
直接使用经试验是不可行的,如:
friend template<class xx, class yy>int operator+(xx , yy )
{
return b + a;
}
无论是像上面这样写(把函数定义放在类的public中)或者是用template<class xx, class yy>int operator+(xx , yy )替换int operator+(int a, Skill b)都是不行的,编译器提示不允许使用template
常用的友元:重载<<运算符
首先,我们知道,<<是一个运算符,且他可以被重载。
其次,我们知道,一般我们这么用cout<< abc;于是,运算符前面有cout,后面有变量abc。就像使用加法运算符重载一样,我们可以仿照着去写。
于是有了友元函数(假如是Skill类):
friend void operator<<(std::ostream& os, const Skill&b); //函数原型
其中,cout是ostream类我们是知道的(在模板时用过,输出到文件或者屏幕),第二个参数是Skill类的引用,被const限定(因此不能被修改)。我们也是知道的。
于是,可以将其放在Skill类的public里作为Skill类的友元函数。
我们编写定义(假设Skill类有两个私有成员,分别是string name和int combat):
void operator<<(std::ostream &os, Skill & b)
{
os << "name:" << b.name << " , combat is " << b.combat << std::endl;
}
注意,这里要加std:: 因为是ostream类在名称空间std之中。
于是我们可以敲代码:
Skill m;
cout << m;
调用了友元函数,搞定。
但假如因为实际需要,没有在运算符重载的函数定义里的最后输出<<std::endl;
那么在实际使用之中,我们要换行的话,就得这么输入cout<<m<<endl;
似乎这样应该是可以的?
但并不是这样。
原因在于:
<<本身面对cout时,已经被运算符重载了。我们实际经验来看,cout可以输出int,double,long,string类,char类等。
之所以能输出这些,是因为在ostream类中,有所有基本类型的<<运算符重载。
也就是说,就像我们在Skill类进行了<<对skill类的运算符重载一样,ostream类也对<<对所有基本类型进行了运算符重载。
而string类虽然不是基本类型,但是我们也可以像使用基本类型那样,对string类对象进行cout来输出,这说明,string类也进行了<<运算符重载。
运算符重载的实质,是调用函数。
例如对Skill类的对象m使用cout<<m;
实质上是调用了函数operator<<(cout, m);这个函数。
这个函数会输出一段文字,于是cout<<m便输出了cout和m作为参数时输出的文字。
了解了这个实质的前提下,我们又知道,程序是从左往右运行的(在优先级相同的情况下)。那么cout<<m<<endl; 就变成了(operator<<(cout, m))<<endl;
endl能直接使用么?显然不行,我们需要换行的时候,一般是这么输入:cout<<endl;
而void operator<<(std::ostream &os, Skill & b)这个函数,是无返回值的,因此不能与<<endl;使用。
(注1:<<运算符最初的目的不是输出,而是C和C++的位运算符,将值中的位左移)
(注2:就像我们不能直接用<<endl一样,并没有这样的运算符重载。注意,运算符在使用时,有一个规则是不能违反原来的使用,例如<<必然是左右两个,+的左右也必然是两个,而不是说+a这样使用)
于是,给<<这个运算符重载一个返回值,且这个返回值是ostream类,那么我们就可以继续愉快的使用了。
即std::ostream & operator<<(std::ostream &os, Skill & b)
这样。他返回了一个ostream类引用。
另外,不要加const ,推测是因为没有const ostream&的重载函数。
然后新的<<运算符重载定义是:
std::ostream& operator<<(std::ostream &os, Skill & b)
{
os << "name:" << b.name << " , combat is " << b.combat << std::endl;
return os; //输入什么类型的ostream类对象,就输出什么对象
}
如代码:
#include<iostream> #include<string> class Skill { std::string name; int combat; public: Skill(std::string na= "迪克",int b = 1) { name = na;combat = b; } //默认构造函数 friend std::ostream& operator<<(std::ostream &os, Skill & b); }; int main() { using namespace std; Skill m; Skill n("李察", 10); cout << m << endl << n << endl; //因为返回os,所以在遇见非Skill类时,使用ostream类自己定义的运算符重载 system("pause"); return 0; } std::ostream& operator<<(std::ostream &os, Skill & b) { os << "name:" << b.name << " , combat is " << b.combat; return os; //输入什么类型的ostream类对象,就输出什么对象 }
显示:
name:迪克 , combat is 1 name:李察 , combat is 10 请按任意键继续. . .
一切正常。
看到这里,好像友元函数说着说着就又跳到运算符重载了。
但再次强调,友元函数的存在意义,就是让运算符的第一个参数,可以是非类的成员。例如cout、int等,都不是Skill类的成员,如果不使用友元函数,那么是无法进行运算符重载的。
回过头来重看友元函数的存在意义和不使用友元函数的后果:
①不使用友元函数则不能调用类的私有成员(友元函数的访问权限同类的成员函数);
②不使用友元函数不能让非类成员在运算符前面,原因看③和④;
③当类成员在运算符前面时,调用的是运算符重载函数,在后面的作为运算符重载的参数,如:Skill Skill::operator+(const Skill&b)const
④当非类成员在运算符前面时,无法调用类成员函数——因为很多是在ostream类定义的 (例如cout<<a;这段话的具体应用,应看下面,单纯看这段话是看不懂的) ,于是,他只能调用类外对应的函数重载了。但类是我们自己定义的,毫无疑问,比如ostream类并不知道我们说的是什么,因为他不认识我们自定义的类,所以无法输出,或者进行加减;
⑤因此,我们需要自定义一个运算符重载函数,让类对象作为参数,但是一般情况下,这个运算符重载函数是不能访问类对象的私有成员的,因此必须让他拥有能访问这个类对象私有成员的权限——也就是友元函数的意义。
关键:一个运算符时,在运算符前面的决定运算符重载函数的调用。
重载运算符:作为成员函数还是非成员函数:
现在有两种运算符重载的函数格式:(假设类为Player,类对象为m)
一种是面对成员函数的,例如: void operator+(int a);
一种是面对非成员函数的,例如:friend void operator +(int a, Player &b);
前者在调用时:m.operator +(int a); //一个参数,另一个为类对象被隐式的传递了
后者在调用时:operator +(int a, Player &b); //两个参数
什么时候调用非成员函数(友元函数)呢?三种情况:
①运算符有两个类对象,且都是同一个类。使用成员函数,且使用一个参数(我的编译器VS2015,提示成员函数的运算符重载函数只能使用一个参数)
③运算符有两个类对象时,且不是同一个类,那么不能使用即是成员函数,又是友元函数的形式(即是一个类的成员函数且是另一个类的友元函数,是不行的,至少我尝试了不行)。
但可以考虑使用 成员函数的返回值 的形式,变相得到我们需要的值。例如我们需要B类的私有成员aa,那么我们可以在A类的成员函数中,B类作为参数,函数定义中,使用B类能返回aa值的成员函数,从而得出结果。如代码:
#include<iostream> class Player { int combat; public: Player() { combat = 5; } int getcombat() { return combat; } //成员函数,返回值为私有成员的值 }; class Skill //因为Skill类的成员函数需要调用Player类,所以其必须在Player类的声明之后 { int combat; public: Skill() { combat = 1; }; int operator +(Player& m); }; int main() { using namespace std; Skill m; Player n; cout << m + n << endl; system("pause"); return 0; } int Skill::operator +(Player&m) //类对象作为参数,进行运算符重载 { int q; q = combat+m.getcombat(); //调用类方法getcombat() return q; }
③一个类对象和一个基本类型。
假如类对象在前,基本类型在后:一般使用成员函数(因为这样更简单),也可以使用友元函数,例如:friend int operator +(const Skill& m,const int b);但这样就相对复杂一些,个人觉得意义不大。
假如基本类型在前,类对象在后:只能使用友元函数。