友元,顾名思义,是某个类的“朋友”。这个朋友角色的特殊之处在于它可以访问类中所有的成员,包括私有、保护和公有的成员函数和成员变量。这似乎违背了类的封装特性,但这就是友元作为单独的机制出现的原因。有些类需要与其他类进行深♂度 互动,有些函数需要遍历所有类中的成员,这些都是数据封装的例外情况,但也是程序设计中真实会发生的情况。针对这类情况,友元的作用就得以体现了。
在C++中类的友元共有三大类型:
- 友元类
- 友元函数
- 友元成员函数
友元的声明
友元的使用情况具有特殊性,所以所有的友元关系都是单方面的,并且采用授权制。即仅允许一个类设置其他类或函数成为友元,不允许外界将它设置成为友元。
打个比方,封装好的数据有相当一部分都是私有的(private),属于私人的东西仅可以由他自己单方面授权给其他人看,而不允许其他人自作主张的将别人作为友元并且翻看它的私人物品。
由于友元仅表示类与函数或类与其他类的特殊关系,因此友元的声明不存在访问控制。也就是说友元的声明可以出现在类中的任意地方而不必考虑是否在public
还是在private
下。
将某元素声明成为类的友元,只需要使用关键字friend
进行修饰即可:
友元函数
一个类可以将一个普通的函数当做友元,允许这个函数访问该类的内部成员。声明方法为:friend 返回类型 函数名(参数列表);
如下面程序:
#include<iostream>
using namespace std;
class A{
friend void print(const A&); //为类A设置友元函数print,允许其访问A类的所有成员
public:
A(int k = 99):k(k){}
private:
int k;
};
void print(const A& a)
{
cout << a.k; //打印A类对象的私有成员k
}
int main()
{
A a;
print(a);
}
结果如下:
友元函数的一种常见使用,就是在类外重载<<
符号以配合cout
使用,达到输出对象内容和连续使用的目的。
#include<iostream>
using namespace std;
class A{
friend ostream& operator<<(ostream&,const A&); //声明重载运算符<<函数为友元函数
public:
A(int k,int a):k(k),a(a){}
private:
int k;
int a;
};
ostream& operator<<(ostream &os,const A &a) //在重载运算符<<函数中打印A类对象的内容并返回ostream对象引用以连续打印
{
os << "the value of k is " << a.k << endl;
os << "the value of a is " << a.a << endl;
os << "-----------------" << endl;
return os;
}
int main()
{
A a1(10,20);
A a2(30,50);
A a3(99,100);
cout << a1 << a2 << a3;
}
运行结果如下:
可以看到,在这个例子中,我们将operator<<
函数设置成为A类的友元函数,使得A类对象能够像内置类型那样参与cout
的输出。
另外,在类中直接重载这个函数并不能实现这个要求。因为在类中重载的<<
将会在对象作为该双目运算符的前一个参数时调用,也就是这样:a << cout;
这么一来就很奇怪了,不能满足我们的要求。
友元类
友元类是将另一个类看作友元,在友元类中可以任意访问原类的成员。声明方法为:friend class 类名
如下面的程序:
#include<iostream>
using namespace std;
class B; //前置声明
class A{
friend class B; //声明B类为友元类
public:
A(int k):k(k){}
private:
void show()const{cout << k << endl;}
int k;
};
class B{
public:
void setK(A& a,const int k){ //访问A类的私有数据成员
a.k = k;
}
void showK(const A& a){ //访问A类的私有成员函数
a.show();
}
};
int main()
{
A a(10086);
B b;
b.showK(a);
b.setK(a,10010);
b.showK(a);
}
结果:
友元类的使用并不只看上去那么简单,主要问题在于类的声明,后面会讨论到。
友元成员函数
一个类可以将另一个类的某个成员函数声明为友元成员函数,声明方式如下:friend 返回类型 所属类::成员函数名称(参数列表);
可以看到,相较于一般函数,成员函数在声明时需要使用作用域解析符指明该成员函数所属的类。
另外,考虑到成员函数可以看作是作用域为某个类的函数,声明一个友元成员函数和友元函数并没有什么区别。
我们将刚刚的程序稍作改变:
#include<iostream>
using namespace std;
class A; //A类的前置声明
class B{
public:
void setK(A&,const int);
void showK(const A&);
};
class A{
friend void B::setK(A&,const int); //将B类的setK函数声明为友元成员函数
friend void B::showK(const A&); //将B列的showK函数声明为友元成员函数
public:
A(int k):k(k){}
private:
void show()const{cout << k << endl;}
int k;
};
inline void B::showK(const A & a){
a.show();
}
inline void B::setK(A& a,const int k){
a.k = k;
}
int main()
{
A a(10086);
B b;
b.showK(a);
b.setK(a,10010);
b.showK(a);
}
运行结果同刚才相同:
类的声明和定义问题
使用友元的主要问题在于相关类的声明和定义究竟应该放在哪里。
例如,在类A中将类B设置成为友元,则在A类声明之前,编译器必须知道B类的存在。而B类使用A类对象的内容有需要的到完整地A类声明。这就需要在A类之前使用B类的前置声明:
class B;
class A{
friend class B;
..
..
};
class B{
'''
...
};
那么,不同行为对类声明的需求如下:
- 声明友元类,只需要知道该类的存在——前置定义
- 使用类对象作为参数声明函数,只需要知道该类存在——前置定义
- 使用类对象作为参数给出函数定义,需要知道类的完整内容——引用头文件或将类声明写在调用类前面
基于这三个要求,我们来回顾一下前面举出的三个程序示例。
友元函数
在友元函数中。
- 友元函数声明形式需要用到类对象作为参数,这需要在编译函数之前对类做前置声明
- 友元函数的实现需要完整的类,这需要在编译函数之前得到类的内容。
所以,下面的两种写法都是可以的:
class A{
friend void print(const A&);
...
};
void print(const A& a){
...
}
class A; //前置声明类A
void print(const A&); //前置声明函数f
class A{
friend void print(const A&);
...
}
void print(const A& a){
...
}
但是这种写法就是不可以的:
void print(const A& a){ //不合法,在编译该函数时不知道A类的存在
...
}
class A{
friend void print(const A&);
...
};
友元类&友元成员函数
友元类的主要问题还是成员函数,而在友元成员函数情况中:
- 声明友元的类需要知道友元类的存在
- 友元类声明成员函数需要知道对方类的存在
- 友元列给出成员函数定义需要知道对方类的全部内容
因此,下面的写法是可行的:
class B; //前置声明B类
class A{
friend class B;
...
};
class B{
public:
void print(const A& a){ //声明并给出成员函数的内联实现
...
}
};
class A; //前置声明A类
class B{
public:
void print(const A&);//仅给出成员函数声明
...
};
class A{
friend class B;
...
};
void B::print(const A& a){//在得知A类具体信息后给出成员函数实现(也可以使用inline使其成为内联的)
...
}
但是下面的写法就是不可以的:
class A; //前置声明A类
class B{
public:
void print(const A&){ //在声明类时直接给出函数的内联实现,这是不合法的,程序编译到这里并不知道A类对象里面有什么
...
}
...
};
class A{
friend class B;
...
};
当两个类互为友元时
当两个类互为友元时:
- 双方声明对方为友元都需要预先知道对方的存在。
- 双方声明使用对方的成员函数时都需要知道对方的存在
- 双方实现使用对方的成员函数是都需要知道对方的具体内容
因此在两个类互为友元的情况下,最好将所有成员函数的声明和实现都分开来,同时两个类声明时都加上对方的前置声明。
下面两种写法都是允许的:
/*AB类的前置声明*/
class A;
class B;
/*AB类的声明*/
class A{
friend class B;
public:
void printB(const B&);
...
};
class B{
friend class A;
public:
void printA(const A&);
...
};
/*双方成员函数的实现*/
void A::printB(const B & b){
...
}
void B::printA(const A & a){
...
}
/*A.h*/
...
class B;
class A{
friend class B;
public:
void printB(const B&);
...
};
...
/*A.cpp*/
#include"B.h"
void A::printB(const B & b){
...
}
/*B.h*/
...
class A;
class B{
friend class A;
public:
void printA(const A&);
...
};
...
/*B.cpp*/
#include"A.h"
void B::printA(const A& a){
...
}
致谢:面向对象课程陈老师,十分认真负责,许多内容是他教授给我的。
看完文章,来关注博主一起学习鸭~~~~
啃书系列往期博客
语言基础部分:
- 啃书《C++ Primer Plus》之 C++ 函数指针
- 啃书《C++ Primer Plus》之 C++ 名称空间1
- 啃书《C++ Primer Plus》之 C++ 名称空间2
- 啃书《C++ Primer Plus》之 C++ 引用
- 啃书《C++ Primer Plus》之 const修饰符修饰 类对象 指针 变量 函数 引用
- 啃书《C++ Primer Plus》之 枚举 内容大全
面向对象部分:
- 啃书《C++ Primer Plus》 面向对象部分 构造函数基础及其使用 ——初始化列表 构造函数重载与调用 创建对象
- 啃书《C++ Primer Plus》 面向对象部分 类型转换——转换构造函数 与 转换函数
- 啃书《C++ Primer Plus》 面向对象部分 析构函数
- 啃书《C++ Primer Plus》 面向对象部分 深拷贝与浅拷贝问题 拷贝构造函数 赋值函数
- 啃书《C++ Primer Plus》 动态内存管理(上) new和delete的使用
- 啃书《C++ Primer Plus》 面向对象部分 动态内存管理(中) 动态对象的创建 重载new和delete
- 啃书《C++ Primer Plus》 面向对象部分 动态内存管理(下) 动态成员管理
- 啃书《C++ Primer Plus》面向对象部分 静态联编与动态联编
- 啃书《C++ Primer Plus》 面向对象部分 虚机制——虚函数表、虚指针