(一二二)友元函数

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

 

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

 

时间: 2024-10-21 23:04:18

(一二二)友元函数的相关文章

C++学习摘要之六:友元函数与友元类

采用类的机制后实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,成员函数一般定义为 公有的,依此提供类与外界间的通信接口.但是,有时需要定义一些函数,这些函数不是类的一部分,但 又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数.除了友元函数外,还有 友元类,两者统称为友元.友元的作用是提供了工序的运行效率,但又破坏了类的封装性. 1.友元函数 友元函数是可以直接访问类的私有成员的非成员函数.它是定义在类外的普通函数,它不属于任何类 ,但需要在类的定义中加以声明,声明时

c++-类的友元函数是命名空间的成员吗?

问题描述 类的友元函数是命名空间的成员吗? 在一个名称空间里面声明了一个类,在名称空间外定义,在这个类里面声明了一个友元函数,那么这个友元函数是不是原来名称空间的成员? 因为我实现这个友元的时候没有加命名空间限定,它自己认为自己是名称空间里的成员. 另外(与上无关),类的友元函数是不用事先声明的吧,现在类写到这了,需要友元,我就声明个友元,然后在类后面实现它就好了,是这样吗?或者我也可以事先声明,在类前声明,在类里加friend,类后再实现也是可以的是吗? 解决方案 看友元函数自己的定义,放在命

图片-C++友元函数 大家看看问题错在哪里

问题描述 C++友元函数 大家看看问题错在哪里 错字哪里啊 解决方案 add函数的参数复制方式,不会修改原对象,看看书上的交换值的例子就清楚了. 解决方案二: add的参数用引用,不然函数修改的是复制的对象.

c++基础-C++关于友元函数问题(VS2013编译环境提示错误)

问题描述 C++关于友元函数问题(VS2013编译环境提示错误) 这段代码用VS2013调试出来能够有黑窗口,且正常运行,但是VS2013总是提示错误,说是Date类的month,day , year不可访问,他们虽然声明为private,但是友元 函数不能访问Date类的私有成员吗? #include using namespace std; class Date; class Time { public: Time(int, int, int); void display(Date &);

C++之:友元函数

一.定义 友元函数是可以直接访问类的私有成员的非成员函数.它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下: friend 类型 函数名(形式参数); 友元提供了不同类的成员函数之间.类的成员函数与一般函数之间进行数据共享的机制.通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员.c++中的友元为封装隐藏这堵不透明的墙开了一个小孔,外界可以通过这个小孔窥视内部的秘密. 友元的正确使用能提高程

c++-求两个日期相差的天数 用友元函数 我不知道要怎么改 看不懂调试。

问题描述 求两个日期相差的天数 用友元函数 我不知道要怎么改 看不懂调试. #include class Date {int yearmonthday;public : void show(); int set(int aint bint c); friend int End(Date &d1 Date &d2);};void Date::show (){cout<<""please input year monthday.""<}

link中是否能定义友元函数?友元函数是不是可以访问成员函数?

问题描述 link中是否能定义友元函数?友元函数是不是可以访问成员函数? link中是否能定义友元函数?友元函数是不是可以访问成员函数? 解决方案 C#不支持友元.VB倒是支持,不过和C++的友元不是一回事.VB的友元相当于C#的internal如果你想让一个类操作另一个类的私有成员,可以定义为嵌套类 解决方案二: 可以.友元.(公共)成员.私有函数只是可见性不同,没有本质的不同.除非是静态函数,不能调用非静态函数.

C++运算符重载问题友元函数

问题描述 C++运算符重载问题友元函数 #include#includeusing namespace std;class Complex{ public: Complex(double newx=0.0, double newy=0.0 ); //Complex(); Complex(Complex &c); double getx() const; double gety() const; Complex Add(const Complex &another); void Output

c+-C++运算符重载问题友元函数

问题描述 C++运算符重载问题友元函数 #include#includeusing namespace std;class Complex{ public: Complex(double newx=0.0 double newy=0.0 ); //Complex(); Complex(Complex &c); double getx() const; double gety() const; Complex Add(const Complex &another); void Output(