(一二二)友元函数

简介:

由于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);但这样就相对复杂一些,个人觉得意义不大。

 

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

 


目录
相关文章
|
C++
32 C++ - 运算符重载碰上友元函数
32 C++ - 运算符重载碰上友元函数
28 0
|
算法 编译器 C++
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(中)
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(中)
60 0
|
存储 算法 编译器
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(上)
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(上)
49 0
|
5月前
|
Java 开发者
那些年,我们追过的Java多态——回忆篇
【6月更文挑战第17天】重温Java多态,它激发了初学者对面向对象编程的热情。多态展示了代码的灵活性和可扩展性,通过抽象和接口使设计更高效。在实践中,如GUI事件处理和游戏开发,多态广泛应用。随着时间的推移,理解加深,多态被视为反映现实多样性的编程哲学。对初学者,它是探索编程世界的钥匙,不应惧怕困惑,应多实践,享受与计算机对话的乐趣。多态,是编程旅程中宝贵的财富和成长见证。
28 0
|
搜索推荐 编译器 C++
【C++从0到王者】第三站:类和对象(中)构造函数与析构函数
【C++从0到王者】第三站:类和对象(中)构造函数与析构函数
40 0
|
编译器 C++
【C++从0到王者】第三站:类和对象(中)赋值运算符重载
【C++从0到王者】第三站:类和对象(中)赋值运算符重载
51 0
|
缓存 算法 安全
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(下)
类与对象知识总结+封闭类+const+this指针 C++程序设计与算法笔记总结(三) 北京大学 郭炜(下)
44 0
|
6月前
|
存储 编译器 程序员
【C/C++ this指针 20240105更新】探索C++编程之旅:深入理解this指针的魅力与应用
【C/C++ this指针 20240105更新】探索C++编程之旅:深入理解this指针的魅力与应用
77 0
|
6月前
|
C++
第十二章:C++中的this指针详解
第十二章:C++中的this指针详解
48 0
|
6月前
一文搞懂友元函数和友元类
1.友元概念 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
96 0
下一篇
无影云桌面