1. 友元基本概念以及诞生原因
通常,公有类方法提供唯一的访问途径,但是有时候这种制太严格,以致于不适合特定的编程问题。
在这种情况下,C+提供了另外一种形式的访问权限: 友元
友元有3种:
- 友元函数;
- 友元类
- 友元成员函数
过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
这篇文章主要讨论友元函数
在为类重载二元运算符时(带两个参数的运算符)常常友元。
将Demo对象乘以实数就属于这种情况,下面来看看Demo类的定义。
class Demo {
private:
int iAge;
string name;
public:
Demo(int i, const string & name);
void Show();
Demo operator+(const Demo &d) const;
Demo operator*(int i) const;
};
并有下面这样一个例子
#include <iostream>
#include <<tring>
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
using namespace std;
class Demo {
private:
int iAge;
string name;
public:
Demo(int i, const string & name);
void Show();
Demo operator+(const Demo &d) const;
Demo operator*(int i) const;
};
void Demo::Show()
{
cout << "iAge:" << iAge << endl;
}
Demo::Demo(int i = 0, const string & name = "null")
{
this->iAge = i;
this->name = name;
}
Demo Demo::operator+(const Demo &d) const
{
Demo result;
cout << "this->name:" << this->name << ", this->age: " << this->iAge << ", d.name:" << d.name << ", d.iAge: " << d.iAge << endl;
result.iAge = this->iAge + d.iAge;
return result;
}
Demo Demo::operator*(int i) const
{
Demo result;
result.iAge = this->iAge * i;
return result;
}
int main(int argc, char *argv[])
{
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
Demo d0 = Demo(0, "d0");
Demo d1(1, "d1");
d0 = d1 * 4;
d0.Show();
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
return 0;
}
这里是能够运行成功的。
meng-yue@ubuntu:~/MengYue/c++/use_class/02$ ./friend
---------------开始--->公众号:梦悦foundation---------------
iAge:4
---------------结束--->公众号:梦悦foundation---------------
d0 = d1 * 4;
这一行代码 会调用 d1的 重载 * 函数(d1.operator**(4)
)。也就是说类对象必须放到前面。
假设是下面这样呢?
d0 = 4 * d1
会出现什么问题??
他会提示没有找到一个 具有 int类型的 Demo类型的 * 重载函数。
meng-yue@ubuntu:~/MengYue/c++/use_class/02$ g++ -o friend friend.cpp
friend.cpp: In function ‘int main(int, char**)’:
friend.cpp:55:9: error: no match for ‘operator*’ (operand types are ‘int’ and ‘Demo’)
d0 = 4 * d1;
~~^~~~
meng-yue@ubuntu:~/MengYue/c++/use_class/02$
记住,左侧的操作数应是调用对象,但2.75不是对象。因此,编译器不能使用成员函数调用来
表达式决这个难题的一种方式是,告知每个人(包括程序员自己),只能按d0 = d1 * 4
这种格式编写,不能写 d0 = d1 * 4
。
然而,还有另一种解决方式——非成员函数
(记住,大多数运算符都可以通过成员或非成员函数来重载, 非成员函数不是由对象调用的,它使用的所有值(包括对象) 都是显式参数。这样,编译器能够将下面的表达式
d0 = 4 * d1
与下面的非成员函数调用匹配
d0 = operator*(4, d1);
该函数的原型如下:
Demo operator*(int i, const Demo &d);
对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算达式右边的操作数对应于运算符函数的第二个参数。
使用非成员函数可以按所需的顺序获得操作数(先是 double,然后是Demo),但引发了一个新问题函数不能直接访问类的私有数据,至少常规非成员函数不能访问(不能使用d.iAge访问变量了)。然而,有一类特殊的非成员函数方问类的私有成员,它们被称为友元函数。
2. 创建友元
创建友元元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend
friend Demo operator+(double m, const Demo & t); //goes in class declaration
原型意味着下面两点:
- 虽然 operator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
- 虽然 operator*()函数不是成员函数,但它与成员函数的访问权限相同。
第二步是编写函数定义。
因为它不是成员函数,所以不要使用Demo::
限定符。另外,不要在定义中使用关键字 friend,定义应该如下:
Demo operator*(int i, const Demo &d)//friend not used in definition
有了上述声明和定义后,下面的语句
d0 = 4 * d1;
将转换为如下语句,从而调用刚才定义的非成员友元函数
d0 = operator*(4, d1);
总之,类的友元函数是非成员函数,其访问权限与成员函数相同
friend01.cpp
定义一个这样的函数
Demo operator*(int i, const Demo &d)//friend not used in definition;
#include <iostream>
#include <<tring>
/*
author:梦悦foundation
公众号:梦悦foundation
可以在公众号获得源码和详细的图文笔记
*/
using namespace std;
class Demo {
private:
int iAge;
string name;
public:
Demo(int i, const string & name);
void Show();
Demo operator+(const Demo &d) const;
Demo operator*(int i) const;
};
void Demo::Show()
{
cout << "iAge:" << iAge << endl;
}
Demo::Demo(int i = 0, const string & name = "null")
{
this->iAge = i;
this->name = name;
}
Demo Demo::operator+(const Demo &d) const
{
Demo result;
cout << "this->name:" << this->name << ", this->age: " << this->iAge << ", d.name:" << d.name << ", d.iAge: " << d.iAge << endl;
result.iAge = this->iAge + d.iAge;
return result;
}
Demo Demo::operator*(int i) const
{
Demo result;
result.iAge = this->iAge * i;
return result;
}
Demo operator*(int i, const Demo &d)
{
cout << "operator*(int i, const Demo &d)" << endl;
}
int main(int argc, char *argv[])
{
cout << "---------------开始--->公众号:梦悦foundation---------------" << endl;
Demo d0 = Demo(0, "d0");
Demo d1(1, "d1");
d0 = 4 * d1;
d0.Show();
cout << "---------------结束--->公众号:梦悦foundation---------------" << endl;
return 0;
}
编译运行的结果:
meng-yue@ubuntu:~/MengYue/c++/use_class/02$ ./friend01
---------------开始--->公众号:梦悦foundation---------------
operator*(int i, const Demo &d)
iAge:-1791702336
---------------结束--->公众号:梦悦foundation---------------
Segmentation fault (core dumped)
meng-yue@ubuntu:~/MengYue/c++/use_class/02$
从结果来看,这个定义的函数成功的被调用了。再对它进行修改成友元函数。
friend02.cpp
class Demo {
private:
int iAge;
string name;
public:
Demo(int i, const string & name);
void Show();
Demo operator+(const Demo &d) const;
Demo operator*(int i) const;
friend Demo operator*(int i, const Demo &d);
};
Demo operator*(int i, const Demo &d)
{
Demo result;
result.iAge = d.iAge * i;
cout << "operator*(int i, const Demo &d)" << endl;
return result;
}
运行结果!
meng-yue@ubuntu:~/MengYue/c++/use_class/02$ ./friend02
---------------开始--->公众号:梦悦foundation---------------
operator*(int i, const Demo &d)
iAge:4
---------------结束--->公众号:梦悦foundation---------------
meng-yue@ubuntu:~/MengYue/c++/use_class/02$
友元函数和 重载的 *
成员函数 结果是一样的!
3. 常用的友元: 重载 <<
运算符
一个很有用的类特性是,可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。
假设trip是一个 Demo 对象,为显示trip的值, 前面使用的是Show()。
然而,如果可以像下面这样操作将更好
cout << trip: // make cout recognize Demo class?
之所以可以这样做,是因为<<是可被重载的C+运算符之一。
实际上,它已经被重载很多次了。最初,<<运算符是C和C++的位运算符,将值中的位左移。 ostream类对该运算符进行了重载,将其转换为一个输出工具。
cout是一个 ostream对象,它是智能的,能够识别所有的C++基本类型。
这是因为对于每种基本类型, ostream类声明中都包含了相应的重载的 operator<<()定义。也就是说一个定义使用int参数,一个定义使用 double参数,等等。
因此,要使cout能够识别Demo对象,一种方法是将一个新的函数运算符定义添加到 ostream类声明中。
但修改 iostream文件是个危险的主意,这样做会在标准接口上浪费时间。
相反,通过Demo类声明来让Demo类知道如何使用cout
1. <<的第一种重载版本
要使Demo类知道使用cout,必须使用友元函数。这是什么原因呢?因为下面这样的语句使用两个对象,其中第一个是 ostream类对象(cout):
cout << trip;
如果使用一个Demo成员函数来重载<<, Demo对象将是第一个操作数,就像使用成员函数重载*
运算符那样。这意味着必须这样使用
trip << cout: //if operator<<() were a Demo member function
这样会令人迷惑。但通过使用友元函数,可以像下面这样重载运算符:
void operator<<(ostream & os, const Demo &d)
{
os << "iAge:" << d.iAge << endl;
}
friend03.cpp
class Demo {
private:
int iAge;
string name;
public:
Demo(int i, const string & name);
void Show();
Demo operator+(const Demo &d) const;
Demo operator*(int i) const;
friend Demo operator*(int i, const Demo &d);
friend void operator<<(ostream &os, const Demo &d);
};
void operator<<(ostream &os, const Demo &d)
{
os << "operator<<, iAge:" << d.iAge << endl;
}
使用cout << d0;
可以直接操作 类,非常方便
编译运行的结果:
meng-yue@ubuntu:~/MengYue/c++/use_class/02$ ./friend03
---------------开始--->公众号:梦悦foundation---------------
operator*(int i, const Demo &d)
iAge:4
operator<<, iAge:4
---------------结束--->公众号:梦悦foundation---------------
meng-yue@ubuntu:~/MengYue/c++/use_class/02$
2. <<的第二种重載版本
前面介绍的实现存在一个问题。像下面这样的语句可以正常工作:
cout << trip;
但这种实现不允许像通常那样将重新定义的 << 运算符与cout一起使用:
cout << " Trip time:" << trip << "(Tuesday)\n"; //can't do
要理解这样做不可行的原因以及必须如何做才能使其可行,首先需要了解关于cout操作的一点知识。
请看下面的语句:
int x = 5;
int y =8;
cout << x << y;
C++从左至右读取输出语句,意味着它等同于:
(cout <<) << y;
正如 iostream中定义的那样,<<运算符要求左边是一个 ostream对象。
显然,因为cout是 ostream对象,所以表达式cout<<x满足这种要求。
然而,因为表达式cout<<x位于<<y的左侧,所以输出语句也要求该表达式是一个 ostream类型的对象。因此, ostream类将 operator<<()函数实现为返回一个指向 ostream对象的引用。
具体地说,它返回一个指向调用对象(这里是cout)的引用。
因此,表达式(cout<<x)本身就是ostream对象cout,从而可以位于<<运算符的左侧。
可以对友元函数采用相同的方法。只要修改 operators<<()函数,让它返回 ostream对象的引用即可
ostream & operator<<(ostream &os, const Demo &d)
{
os << "operator<<, iAge:" << d.iAge << endl;
return os;
}
注意,返回类型是 ostream &
。
这意味着该函数返回 ostream对象的引用。因为函数开始执行时,程序传递了一个对象引用给它,这样做的最终结果是,函数的返回值就是传递给它的对象。
也就是说,下面的语句
cout << trip;
将被转换为下面的调用
将被转换为下面的调用:
operator<<(cout, trip;
而该调用返回cout对象。因此,下面的语句可以正常工作
cout < " Trip time:" << trip << "(Tuesday)\n"; //can't do
我们将这条语句分成多步,来看看它是如何工作的。
首先,下面的代码调用 ostream中的<<定义,它显示字符串并返回cout对象:
cout <<Trip time:
因此表达式 cout <“ Trip time:”将显示字符串,然后被它的返回值cout所替代。
原来的语句被简化为下面的形式
cout < trip << "(Tuesday)\n"; //can't do
接下来,程序使用Demo声明的<< 显示trip值,并再次返回cout对象。
这将语句简化为
cout << "(Tuesday)\n";
现在,程序使用 ostream中用于字符串的<<定义,来显示最后一个字符串,并结束运行。
一般来说,要重载<<运算符来显示 class_name的对象,可使用一个友元函数,其定义如下:
ostream & operator<<(ostream &os, const class_name &obj)
{
os << ...;//display object contents
return os;
}