(一二二)友元函数

简介:

由于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类对象。

问题来了(1int a在哪里?在函数定义里如何书写?

2)因为他不在类成员函数里,那么b.dam这样的调用自然也是不行的(因为只有在成员函数内才能访问私有成员);

 

对第(1)个问题很简单,把int a加到参数里,第一个参数表示加号前面的数字。例如:Skill operator+(int a,const Skill&b);

对于第二个问题,便只能启用友元函数这个概念了。

 

 

友元函数:

①函数原型位于类的公有成员(public)处进行声明;

②函数原型前加friend,函数定义中不加friend

③函数定义前,不加类的定义域解析运算符(例如Skill::这样的,不加);

④重载的运算符使用哪个友元函数,根据对象的类来决定(例如有两个类,SkillPlayer,当调用不同的参数时,编译器会根据运算符重载的几个重载函数,来进行重载函数的参数匹配,寻找到参数类型相符的函数(重载函数的调用,是根据参数的匹配程度来决定的)。

 

如代码:

#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+55+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 aSkill b)都是不行的,编译器提示不允许使用template

 

 

 

 

常用的友元:重载<<运算符

首先,我们知道,<<是一个运算符,且他可以被重载。

其次,我们知道,一般我们这么用cout<< abc;于是,运算符前面有cout,后面有变量abc。就像使用加法运算符重载一样,我们可以仿照着去写。

于是有了友元函数(假如是Skill类):

friend void operator<<(std::ostream& os, const Skill&b); //函数原型

其中,coutostream类我们是知道的(在模板时用过,输出到文件或者屏幕),第二个参数是Skill类的引用,被const限定(因此不能被修改)。我们也是知道的。

于是,可以将其放在Skill类的public里作为Skill类的友元函数。

我们编写定义(假设Skill类有两个私有成员,分别是string nameint combat):

void operator<<(std::ostream &osSkill & 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,doublelongstring类,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 &osSkill & b)这个函数,是无返回值的,因此不能与<<endl;使用。

(注1<<运算符最初的目的不是输出,而是CC++的位运算符,将值中的位左移)

(注2:就像我们不能直接用<<endl一样,并没有这样的运算符重载。注意,运算符在使用时,有一个规则是不能违反原来的使用,例如<<必然是左右两个,+的左右也必然是两个,而不是说+a这样使用)

 

于是,给<<这个运算符重载一个返回值,且这个返回值是ostream类,那么我们就可以继续愉快的使用了。

即std::ostream & operator<<(std::ostream &os, Skill & b)

这样。他返回了一个ostream类引用。

另外,不要加const  ,推测是因为没有const ostream&的重载函数。

 

然后新的<<运算符重载定义是:

std::ostreamoperator<<(std::ostream &osSkill & 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
请按任意键继续. . .

一切正常。

 

看到这里,好像友元函数说着说着就又跳到运算符重载了。

 

但再次强调,友元函数的存在意义,就是让运算符的第一个参数,可以是非类的成员。例如coutint等,都不是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);但这样就相对复杂一些,个人觉得意义不大。

 

假如基本类型在前,类对象在后:只能使用友元函数。

 


目录
相关文章
|
8月前
|
C++
32 C++ - 运算符重载碰上友元函数
32 C++ - 运算符重载碰上友元函数
23 0
|
8月前
|
存储 编译器 C++
【C++从0到王者】第三站:类和对象(中)拷贝构造函数
【C++从0到王者】第三站:类和对象(中)拷贝构造函数
34 0
|
2月前
|
存储 编译器 C++
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
|
8月前
|
编译器 C++
【C++从0到王者】第三站:类和对象(中)赋值运算符重载
【C++从0到王者】第三站:类和对象(中)赋值运算符重载
37 0
|
8月前
|
搜索推荐 编译器 C++
【C++从0到王者】第三站:类和对象(中)构造函数与析构函数
【C++从0到王者】第三站:类和对象(中)构造函数与析构函数
30 0
|
11月前
|
编译器 Linux C语言
【C++精华铺】2.C++入门 缺省参数、函数重载
含有缺省参数的函数在声明的时候要注意声明的时候定义和声明如果是分开的话缺省参数必须写在声明里。
|
2月前
|
C++
第十二章:C++中的this指针详解
第十二章:C++中的this指针详解
33 0
|
2月前
一文搞懂友元函数和友元类
1.友元概念 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
65 0
|
9月前
|
存储 算法 编译器
虚函数和多态+虚析构函数 知识点总结 C++程序设计与算法笔记总结(五) 北京大学 郭炜
虚函数和多态+虚析构函数 知识点总结 C++程序设计与算法笔记总结(五) 北京大学 郭炜
44 0
|
C++
【C++知识点】友元类与友元函数
【C++知识点】友元类与友元函数
66 0