C++ —— 友元函数

C++ 控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但是有时候这种限制太严格,以致于不适合特定的编程问题。在这种情况下,C++ 提供了另外一种形式的访问权限:友元。友元有三种形式 —— 友元函数、友元成员函数、友元类。这里只介绍友元函数。

当函数被声明为类的友元函数,可以赋予该函数与类的成员函数相同的访问权限。

为什么需要友元函数

在为类重载二元运算符时常常需要用到友元函数。
例如,为一个时间类 Time 重载乘法运算符,使其可以乘以一个实数,部分代码如下所示:

class Time {
private:
    int hours;
    int minutes;
public:
    ...;
    Time operator*(double n) const;
    void show() const;
};
void Time::show() const {
    std::cout << this->hours << " hours, " << this->minutes << " minutes. Address is " << this << std::endl;
}
Time Time::operator*(double n) const {
    Time res; 
    long totalMinutes = (long) (n * (hours * 60 + minutes));
    res.hours = totalMinutes / 60;
    res.minutes = totalMinutes % 60;
}

这个重载的乘法运算符使用的是两个不同的类型,即乘法运算符将一个 Time 值与一个 double 值结合在一起。这限制了乘法运算符的使用 —— Time 对象必须在乘法运算符的左侧,double 值必须在乘法运算符右侧。

A = B * 2.5; // 正确
A = 2.5 * B; // 错误

2.5 * B 并不对应 operator*() 成员函数,因为 2.5 不是 Time 对象。

解决这个问题的一种方式是,告知每个人,只能按照 B * 2.5 这种格式编写,这是一种对服务器友好-客户警惕的解决方案。
另一种方式则是采用非成员函数来重载乘法运算符。非成员函数不是由对象调用,它使用的所有值都是显式参数。

使用非成员函数重载乘法运算符可以按照所需的顺序来获得操作数(先 double 值,后 Time 对象),但是非成员函数无法直接访问类的私有数据。可以通过提供一系列的公有接口让非成员函数间接执行一些操作,但更常用的是利用一类特殊的非成员函数 —— 友元函数,友元函数可以直接访问类的私有成员。

创建友元函数

创建友元函数的第一步是将友元函数的原型放在类声明中,并在其原型声明的前面加上关键字 friend。

class Time {
private:
    int hours;
    int minutes;
public:
    ...;
    Time operator*(double n) const;
    friend Time operator*(double, const Time &);
    void show() const;
};

这个原型意味着两点:

  • 虽然该 operator*() 是在类声明中声明的,但它不是成员函数,因此不能用成员运算符来调用;
  • 虽然 operator*() 不是成员函数,但它与成员函数的访问权限相同。

创建友元函数的第二步就是编写函数定义。因为友元函数不是类的成员函数,因此在编写函数定义的时候,不必在函数名之前使用 Time:: 限定符。另外,不要在函数定义处使用 friend 关键字。

Time operator*(double n, const Time & t) {
    Time res;
    long totalMinutes = (long) (n * (t.hours * 60 + t.minutes));
    res.hours = totalMinutes / 60;
    res.minutes = totalMinutes % 60;
    return res;
}

有了上述声明和定义之后,下面的语句:

A = 2.5 * B;

将转换为如下的语句,从而调用刚才定义的非成员友元函数:

A = operator*(2.5, B);

乍一看,友元函数违反了 OOP 数据隐藏的原则,因为友元机制允许非成员函数访问私有数据,但这个观点太片面了,应该将友元函数看做类扩展接口的组成部分。只有在类声明中才能够决定那一个函数是本类的友元函数,因此类声明仍然控制了那些函数可以访问私有成员。总之,类方法和友元只是表达类接口的两种不同机制。

扩展:普通的非成员函数的运算符重载演示

上面的例子只是为了演示友元函数,实际上,在定义了 Time 类的成员函数 operator*(double) 的基础下,可以通过普通的非成员函数来重载乘法运算符。

Time operator*(double n, const Time & t) {
    return t * n;
}

之前的函数定义中显式地访问了 Time 的私有成员,所以该运算符重载必须声明为友元。但这个版本的则是通过调用 Time 类的成员函数 operator*(double) 来实现的,并不需要显式地访问 Time 的私有成员,因此不必声明为友元。不过,将该版本声明为友元也是一个好主意,这样它将成为正式接口的组成部分。

提示:如果要为类重载运算符,并将非类的项作为第一个操作数,则可以用友元函数来反转操作数的顺序。

常用:重载 << 运算符

一个很有用的类的特性是,可以对 << 运算符重载,使之能与 cout 一起来显示对象的内容。与前面介绍的示例相比,这种重载要更复杂些。
假设 time 是一个 Time 对象,为了显示 Time 的内容,前面使用的是 show() 函数,现在希望可通过下面这样的操作来显示 Time 的内容。

std::cout << time << std::endl;

之所以可以这样做,是因为 << 运算符也是 C++ 可以重载的运算符之一。实际上,它已经被重载过很多次了。最初,<< 运算符是 C++ 的左移运算符。ostream 类对该运算符进行了重载,将其转换为一个输出工具。前面讲过,cout 是一个 ostream 类的对象,它是智能的,能够识别所有的 C++ 基本类型,这是因为对于每种基本类型,ostream 类声明中都包含了相应的重载的 operator<<() 成员函数。因此,要是 cout 能够识别 Time 对象,一种方法是将一个新的函数运算符定义添加到 ostream 类声明中,但修改 iostrea 文件是一个危险的注意,这样做会在标准接口上浪费时间。相反,应该让 Time 类声明来让 Time 类知道如何使用 cout —— 在 Time 类中定义一个友元函数。

重载<<运算符(Version 1)

要使 Time 类知道使用 cout,必须使用友元函数。这是因为cout << time语句,使用两个对象,并且第一个对象是 ostream 类对象(cout)。如果是声明为成员函数,则第一个对象必须是 Time 对象;如果是声明为普通的非成员函数,可以使得第一个对象为 ostream 类对象,但是不能直接访问 Time 的私有成员。

class Time {
private:
    int hours;
    int minutes;
public:
    // ...
    friend void operator<<(std::ostream & out, const Time &);
};
void operator<<(std::ostream & out, const Time & t) {
    std::cout << t.hours << " hours, " << t.minutes << " minutes. Address is " << this << std::endl;
}

这样就可以使用下面的语句打印数据了:

std::cout << time;

operator<<() 虽然是友元函数,但是它只在 Time 类声明中被声明,并没有在 ostream 类中声明,因此,它只是 Time 类的友元函数,并不是 ostream 类的友元函数。这也是因为在 operator<<() 的函数定义中直接访问了 Time 类对象的私有数据,而对于 ostream 对象只是当做一个整体来使用,并没有访问私有成员。

重载<<运算符(Version 2)

在 Version 1 中存在一个问题, 虽然cout << time;可以正常使用,但这种实现不允许像通常那样将重新定义的 << 运算符和 cout 一起使用:

cout << "Time is " << time << endl; // 报错

上面的语句,实际上等同于:

((cout << "Time is ") << time) << endl; 

由于 Version 1 中定义的 operator<<() 返回值是 void,因此执行到 ((cout << "Time is ") << time) 时将返回 void,导致最后的 << 运算符左侧是 void,因此报错。

class Time {
private:
    int hours;
    int minutes;
public:
    // ...
    friend std::ostream & operator<<(std::ostream & out, const Time &);
};
std::ostream & operator<<(std::ostream & out, const Time & t) {
    std::cout << t.hours << " hours, " << t.minutes << " minutes. Address is " << &t << std::endl;
    return out;
}

总结

  1. 友元函数的函数原型位于在类声明中,但并不是类的成员函数,在声明函数原型时,需要使用 friend 关键字。
  2. 友元函数的函数定义不需要 friend 关键字和 :: 运算符,和普通非成员函数是一样的。
  3. 友元函数可以直接访问类的私有成员。
  4. 通常在运算符重载中使用友元函数的情况是:重载的运算符的第一个参数不是该类的对象。
  • 9
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值