C++ friend友元函数、友元类详解及完整例程

C++ friend友元函数、友元类详解及完整例程

0. 综述

C++中的友元机制允许类的非公有成员被一个类或者函数访问,友元按类型分为三种
普通非类成员函数作为友元,
类的成员函数作为友元,
类作为友元。

友元包括友元的声明以及友元的定义。友元的声明默认为了extern,意味着友元类或者友元函数的作用域已经扩展到了包含该类定义的作用域,所以即便我们在类的内部定义友元函数也是没有关系的。事实上,友元不属于任何一个类,它可以在类的任何一个位置被声明,无论在public、protected还是private段落被声明,都不会影响友元的使用和访问特性。

友元函数的特点是能够访问类中的所有成员(包括public/protected/private方法和字段)。普通友元函数从语法上看,是一种必须在类体内声明、可定义在类体内或类体外的普通函数,在定义上和调用上与其他普通函数一样,它不属于任何类。普通友元函数可以在类外定义,但必须在类内部声明,声明时在友元的类型前加上friend关键字,如friend void func()。

类具有封装和信息隐藏的特性。只有类的成员函数才能访问类的保护成员和私有成员,程序中的其他函数无法访问保护成员和私有成员。虽然非成员函数可以访问类中的公有成员,但如果将数据成员都定义为公有的,就会破坏类的封装和隐藏的特性。另外,在某些情况下,特别是在对某些成员函数多次调用时,由于参数传递,类型检查和安全性检查等都需要时间开销,从而影响程序的运行效率。为了解决上述问题,提出了一种使用友元的解决方案。

友元可提高程序的运行效率(即减少了类型检查和安全性检查等需要的时间开销),但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。

友元关系是单向的(若要互为友元,必须显式地互相声明为友元),不可继承、不可相互推导的
若A是B的友元,则B不一定是A的友元。
A是B的友元,C继承自A,则C不一定是B的友元。
若A是B的友元,B是C的友元,不能推导出A是C的友元。

1. 友元函数:普通函数作为友元函数

//Pnt.h
#pragma once
class Pnt
{
public:
    Pnt(double x, double y);
    void print();
    friend double CalcDist(const Pnt &a, const Pnt &b);  //声明友元函数
    
private:
    double mX;    //x坐标
    double mY;    //y坐标
}
------------------------------------------------------------------------

//Pnt.cpp
#include "Pnt.h"
#include <cmath>
#include <iostream>
using namespace std;

Pnt::Pnt(double x, double y)
{
    mX = x;
    mY = y;
}

//定义普通类成员函数
void Pnt::print()
{
    cout<< "x=" << mX << "  y=" << mY << endl;
}

//定义普通友元函数
double CalcDist(const Pnt &a, const Pnt &b)
{
   //友元函数内,可以访问类里面的所有字段、方法(包括public/protected/private)
   //若该函数没有声明为友元函数,则:定义时需要加类限定符Pnt::,将之变为成员函数
   double deltX = a.mX - b.mX;
   double deltY = a.mY - b.mY;
   return sqrt(deltX * deltX + deltY * deltY);
}
---------------------------------------------------------------------------

//main.cpp
#include <windows.h>
#include <iostream>
#include "Pnt.h"
using namespace std;

int main(void)
{
    Pnt p1(3.0, 4.0);
    Pnt p2(6.8, 8.0);
    
    //类成员函数调用
    p1.print();
    p2.print();

    //普通友元函数调用
    double dist = CalcDist(p1, p2);
    cout << "Distance between p1 and p2 is " << dist << endl;

    system("pause");
}

上述例程中,Pnt类声明了一个普通友元函数CalcDis,用来计算两点间的距离。声明时前面加friend关键字,说明这是一个友元函数;定义时和普通函数(非成员函数)定义一样。友元函数CalcDis通过Pnt引用访问Pnt类里面的所有变量(public/protected/private均可)。友元函数调用时,直接调用(和普通函数一样),不需要像类成员函数一样需要类对象来调用。

延申:上述例程中,我们使用print成员函数来打印Pnt的两个变量mX和mY(x坐标和y坐标),但如果我们要打印整个类对象(用cout<<pt; //pt是Pnt的一个对象),即用cout输出自定义数据类型,则需要对输出流进行重载。

对上述例程进行修改,完整例程如下:

//重载输出流:实现直接输出Pnt对象的cout, 如:Pnt pt; cout << pt;

//Pnt.h 新增输出流重载
#pragma once
#include <iostream>
using namespace std;

class Pnt
{
public:
    Pnt(double x, double y);
    void print(void);
    
    friend double CalcDist(const Pnt &a, const Pnt &b);
    friend ostream &operator<<(ostream &out, const Pnt &pt);
    
private:
    double mX;
    double mY;
}
---------------------------------------------------------------------------

//Pnt.cpp 新增输出流<<重载,修改打印函数
#include "Pnt.h"
#include <cmath>

Pnt::Pnt(double x, double y)
{
    mX = x;
    mY = y;
}

void Pnt::print(void)
{
    //继续采用默认cout也可以;此处改为用已重载的cout,可直接输出Pnt对象
    //cout<<"x="<<mX<<"  y="<<mY<<endl;
    cout<<*this;
}

double CalcDist(const Pnt &a, const Pnt &b)
{
   double deltX = a.mX - b.mX;
   double deltY = a.mY - b.mY;
   return sqrt(deltX * deltX + deltY * deltY);
}

ostream &operator<<(ostream &out, const Pnt &pt)
{
    out<< "(" << pt.mX << ", " << pt.mY << ")" << endl;
    return out;
}
---------------------------------------------------------------------------

//main.cpp 无修改
#include <windows.h>
#include <iostream>
#include "Pnt.h"
using namespace std;

int main(void)
{
    Pnt p1(3.0, 4.0);
    Pnt p2(6.8, 8.0);
    p1.print();
    p2.print();
    
    double dist = CalcDist(p1, p2);
    cout << "Distance between p1 and p2 is " << dist << endl;
    system("pause");
}

2. 友元函数:类成员函数作为友元函数

类成员函数作为友元,声明友元的时候要用类限定符,所以必须先定义包含友元函数的类,但是在定义友元的函数时候,又必须事先定义原始类。通常的做法先定义包含友元函数的类,再定义原始类,这个顺序不能乱(如果是友元类,则没有这种这种必须)。

名词约定:
原始类 : 声明友元的类,即写friend Type ClassName::FunName();的那个类;
友元函数类:声明和定义成员函数的那个类,即写 Type FunName();的那个类;

完整示例代码如下:

//Pnt.h  声明原始类Pnt
#pragma once

//需要包含 实现类成员友元函数 的类的头文件
//此处不可写作class PntProc
//因为在原始类的有文件里,声明友元函数时需要知道PntProc里面有哪些方法
//而class PntProc仅前向声明这是一个类,不会告知类的结构
#include "PntProc.h"

class Pnt
{
     //若此处不声明友元函数,则PntProc类里对应的函数报错:不能不访问Pnt私有变量
     friend void     Pnt::printPnt(const Pnt &pt);
     friend double   Pnt::calcDist(const Pnt &a, const Pnt &b);
 public:
     Pnt(double x, double y);
 private:
     double mX;
     double mY;
}
---------------------------------------------------------------------------

//Pnt.cpp 定义原始类
#include "Pnt.h"
Pnt::Pnt(double x, double y)
{
    mX = x;
    mY = y;
}
---------------------------------------------------------------------------

//PntProc.h  声明 包含类成员函数作为友元函数 的类
#pragma once

//前向声明原始类:此处不可写作#include "Pnt.h"(否则就交叉包含了)
//此处只需要知道Pnt是一个类即可
//延申:#include "Pnt.h" 把A.h里面的内容包含进来,知道Pnt的所有结构,
//                       可以声明Pnt类的对象,调用Pnt的公有方法、字段等
//      而Class Pnt;  则只是告知编译器这是一个类,只能定义Pnt的指针或引用,
//                      其他都不行(包括定义对象),可规避.h交叉包含的问题,
//                      如果要在PntProc.h里声明Pnt的对象:Pnt mPnt;是错误的! 只能声明为Pnt *mPnt;
//                     这样,在PntProc.cpp中,写上#include "Pnt.h"后,就可以mPnt = new Pnt();得到Pnt对象指针了
class Pnt;

//声明包含类成员友元函数的类
class PntProc
{
public:
     PntProc(void){};
     void printPnt(const Pnt &pt);
     double calcDist(const Pnt &a, const Pnt &b);
}
---------------------------------------------------------------------------

//PntProc.cpp
#include "PntProc.h"
#include <cmath>
#include <iostrem>
using namespace std;
//在cpp中包含 原始类 头文件
#include "Pnt.h"

void PntProc::printPnt(const Pnt &pt)
{
     // printPnt是PntProc的成员函数
     //但由于在Pnt类里声明了该函数作为Pnt的友元函数
     //所以该函数内可以直接访问Pnt类的所有方法、字段(包括public/protected/private)
     //若没有声明为友元函数,则此处编译报错:不能访问mX、mY
     cout<< "(" << pt.mX << ", " << pt.mY << ")" << endl;
}

double PntProc::calcDist(const Pnt &a, const Pnt &b)
{
     //calcDis是PntProc的成员函数
     //但由于在Pnt类里声明了改函数作为Pnt的友元函数
     //所以该函数内可以直接访问Pnt类的所有方法、字段(包括public/protected/private)
     //若没有声明为友元函数,则此处编译报错:不能访问mX、mY
     double deltX = a.mX - b.mX;
     double deltY = a.mY - b.mY;
     return sqrt( deltX * deltX - deltY * deltY );
}
---------------------------------------------------------------------------


//main.cpp
#include <windows.h>
#include <iostream>
#include "Pnt.h"
using namespace std;

int main(void)
{
     Pnt p1(3.0, 4.0);
     Pnt p2(6.0, 8.0);

     PntProc pc;
     pc.printPnt(p1);
     pc.printPnt(p2);
     double dist = pc.calcDist(p1, p2);
     cout<<"Distance between p1 and p2 is "<<dist<<endl;

    system("pause");
}

3. 友元类:类作为友元

类作为友元需要注意的是友元类和原始类之间的相互依赖关系,如果在友元类中定义的函数使用到了原始类的私有变量,那么就需要在友元类定义的文件(.cpp)中包含原始类定义的头文件。

但是在原始类的定义中(包含友元类声明的那个类),就不需要包含友元类的头文件,也不需要在类定义前去声明友元类,因为友元类的声明自身就是一种声明(它告知编译器可以在类外找到友元类)。

当一个类被声明为 原始类 的 友元类 时,原始类中的所有数据、方法(不论是public/protected/private)均可以被友元类访问。友元关系是单向关系,即如果A是B的友元类,但B不一定是A的友元类,除非显式地声明B是A的友元类。

示例代码如下:

//本例程中,我们声明类PntProc是类Pnt的友元类
//如果读者需要两个类互为友元类,可以尝试自己修改实现

//Pnt.h 声明原始类
#pragma once
#include <string>
class Pnt
{
     //声明PntProc作为Pnt的友元类
     //本头文件不能包含PntProc头文件(规避可能的交叉包含),也不需要前向声明
     //友元类的声明本身就是一种声明,告知编译器可以在类外找到友元类
     friend class PntProc;
     
 public:
     Pnt(double x, double y);
     
 private:
     void setX(double x);
     void setY(double y);
     void print(std::string name);
     
     //重载运算符-:必须声明为友元函数,否则参数只能有一个
     //即:不采用友元函数,没法实现重载后p2-p1的实现形式
     //注意,此处的返回值为“值类型”,请读者思考能不能是“引用类型”,为什么。
     friend Pnt operator -(const Pnt &a, const Pnt &b);
     
 private:
      double mX;
      double mY;
}
---------------------------------------------------------------------------

//Pnt.cpp 定义原始类
#include "Pnt.h"
#include <iostream>
using namespace std;

Pnt::Pnt(double x, double y)
{
     mX = x;
     mY = y;
}

void Pnt::setX(double x)
{
    mX = x;
}

void Pnt::setY(double y)
{
    mY = y;
}

void Pnt::print(string name)
{
     cout << "The point " << name << "is (" << mX << ", " << mY << ")" << endl;
}

Pnt operator-(const Pnt &a, const Pnt &b)
{
     return Pnt(a.mX - b.mX, a.mY - b.mY);
}
---------------------------------------------------------------------------


//PntProc.h 声明友元类
//本头文件(声明文件)中不需要包含原始类Pnt的头文件Pnt.h
//声明 作为原始类友元类 的类(友元类)
#pragma once

class PntProc
{
public:
     PntProc(void){};
     void calcDist(void);
}
---------------------------------------------------------------------------


//PntProc.cpp 定义友元类
#include "PntProc.h"
#include <cmath>
#include <iostream>
using namespace std;
//在cpp中包含 原始类 头文件(本类将会用到原始类的成员函数和成员变量)
#include "Pnt.h"

void PntProc::calcDist()
{
     Pnt p1(3.0, 4.0);
     Pnt p2(6.0, 8.0);
     
     //友元类里,原始类的对象可以访问其私有成员函数
     p1.print("p1");
     p2.print("p2");
     
     //调用重载后的减法
     Pnt deltP = p1 - p2;
     
     //友元类里,原始类的对象可以访问其私有成员变量
     double dist = sqrt( deltP.mX  * deltP.mX + deltP.mY * deltP.mY );
     cout<<"Distance between p1 and p2 is "<<dist<<endl;
     
     //友元类里,原始类的对象可以访问其私有成员函数:更新p2点的位置
     p2.setX(4.0);
     p2.setY(5.0);
     p1.print("p1");
     p2.print("p2");
     
     deltP = p1 - p2;
     dist = sqrt( deltP.mX * deltP.mX + deltP.mY * deltP.mY);
     cout<<"Distance between p1 and p2 is "<<dist<<endl;
     ---------------------------------------------------------------------------


//main.cpp
#include <windows.h>
#include <iostream>
#include "PntProc.h"
using namespace std;

int man(void)
{
     PntProc pc;
     pc.calcDist();
     system("pause");
}

运行结果如下:
在这里插入图片描述

4. 小结

回顾C++友元的三种实现方式:
1) 普通友元函数:非成员函数作为某个类的友元函数,声明在类体内部,定义可以在类体内部或者类体外部;
  定义和调用时,与普通函数一样。
2)类成员函数作为友元函数:原始类的成员函数作为另一个类的友元函数;原始类声明文件(.h文件)需要包含友元函数类的头文件,类体里面声明友元函数,声明时需要带类标识符;友元函数类的声明文件(.h文件)中,需要前向声明原始类,但不能包含原始类的头文件,在其定义文件(.cpp文件)中,如果用到了原始类的成员函数或成员变量,则需要包含原始类的头文件,被声明为友元函数的成员函数,正常定义即可,与该类中其他普通成员函数一致。
3) 友元类:一个类作为另一个类的友元类;友元类可以直接访问原始类中的所有方法、字段,包括public/protected/private;友元类的实现要注意原始类与友元类的依赖关系;原始类头文件不需要包含友元类头文件,也不需要前向声明,其类体内部的friend class XX;就是一种声明,告知编译器可以到类外找到友元类; 友元类头文件也不需要包含原始类头文件,其cpp文件中如果需要访问原始类成员,则需要包含原始类的头文件。

其他:
1)重载减号(“-“),以实现类的两个不同对象直接相减时,需要将该重载函数声明为友元函数。
2)本文注释部分详细解释了包含头文件和前向说明的区别。

本文本人学习整理而成,源码均为手敲,且实测过。但由于第一次写CSDN博文,代码插入时总是出意外,故代码为手敲进去的,可能会有疏漏之处。
----------------------转载或引用请注明出处-----------------

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值