C++ 友元函数与友元类

在 C++ 中,类的 private(私有)成员默认只能在当前类的内部访问,外部函数或其他类都无法直接读写它。但在少数场景下,某个外部函数或另一个类与当前类的关系非常紧密,确实需要直接访问它的私有数据。

为了处理这种特殊的协作关系,C++ 提供了 friend(友元)关键字,它的英文原意就是 “朋友”。

你可以把 friend 想象成一张 “点名发放的专属门禁卡”:private 这扇大门依然关着,只是当前类明确点名让某个外部函数或外部类进来,没有被点名的外部代码依然进不去,如下图所示。

C++ 友元专属门禁卡的访问权限类比

在 C++ 中,友元主要包括以下 2 种常见形式:

  • 友元函数:授权某一个特定的外部普通函数,允许它直接访问当前类的 private 和 protected 成员。
  • 友元类:授权另一个类,允许该类的所有成员函数直接访问当前类的 private 和 protected 成员。

C++ 友元函数

在 C++ 中,如果我们在类的内部使用 friend 关键字声明一个普通函数,那么这个函数就成了当前类的 “友元函数”,可以直接访问该类的私有成员和受保护成员。

语法:

class 类名 {
private:
    // 私有成员

    friend 返回值类型 函数名(参数列表);
};

说明:

初学友元函数时,很多小伙伴看到函数声明写在类的花括号里面,容易误以为它变成了类的成员函数。实际上并非如此,友元函数内部并没有 this 指针,我们在使用时要特别分清以下 2 点:

  • 友元函数不是类的成员函数:它本质上依然是一个独立的类外普通函数,类里面的 friend 语句只是给它发了一张 “访问通行证”。
  • 按普通函数的方式定义和调用:我们在类的外部定义该函数时,不需要再加 friend 关键字,也不需要加 类名:: 前缀。在 main() 函数中调用时,我们直接写 函数名(实参) 即可,不能写成 对象名.函数名()。

下面我们通过一个比较两个长方形盒子 Box 面积大小的例子,来看看友元函数的具体用法。

示例 1:使用友元函数

#include <iostream>

class Box {
private:
    int width;
    int height;

public:
    Box(int w, int h) : width(w), height(h) {}

    // 声明 isLarger 为友元函数
    friend bool isLarger(const Box& b1, const Box& b2);
};

// 类外定义普通函数 isLarger
bool isLarger(const Box& b1, const Box& b2) {
    return b1.width * b1.height > b2.width * b2.height;
}

int main() {
    Box box1(4, 5);    // 面积为 20
    Box box2(3, 6);    // 面积为 18

    if (isLarger(box1, box2)) {
        std::cout << "box1 的面积更大\n";
    } else {
        std::cout << "box2 的面积更大或相等\n";
    }
    return 0;
}

运行结果如下。

box1 的面积更大

分析:

在这个例子中,我们在 Box 类内部使用 friend 将外部普通函数 isLarger() 声明为友元函数。虽然 width 和 height 都是私有成员,但获得授权后,isLarger() 在函数体内就可以直接读取 b1.width 和 b2.width。

同时,这里我们把形参写成了常引用 const Box&,既能省去复制整个对象的开销,又能保证函数内部只读取数据而不修改原对象。在调用友元函数时,小伙伴们要注意,下面这种把它当成成员函数来调用的写法是错误的。

box1.isLarger(box2);

当然了,对于比较两个盒子的面积,我们平时直接写一个公开的 getArea() 成员函数就够用了,这里只是为了方便大家看清友元函数的语法。在实际开发中,友元函数最常见的用场是配合下一节要学习的运算符重载(如重载 << 输出运算符)。

提示 friend 声明本身不受访问修饰符的影响。无论我们把 friend 声明写在类的 private、protected 还是 public 区域,授权效果都完全一样。

C++ 友元类

在 C++ 中,除了单独给某一个函数授权之外,我们还可以把整个外部类声明为当前类的 “友元类”。一旦类 A 把类 B 声明为友元类,类 B 中的所有成员函数就都可以直接访问类 A 的私有成员和受保护成员。

语法:

class 类名A {
private:
    // 类 A 的私有成员

    friend class 类名B;
};

说明:

书写时我们要看清授权方向:在类 A 里面写 friend class 类名B;,表示 “类 A 主动把门禁卡发给类 B”,从此类 B 可以访问类 A 的私有数据。哪怕 类名B 的完整定义写在类 A 后面,这句声明也是合法的,编译器会自动识别出 类名B 是一个类。

示例 2:使用友元类

#include <iostream>

class Device {
private:
    int temp;    // 内部温度

public:
    Device(int t) : temp(t) {}

    // 声明 Monitor 为友元类
    friend class Monitor;
};

class Monitor {
public:
    void checkStatus(const Device& dev) const {
        // 直接读取 Device 的私有成员
        std::cout << "内部温度:" << dev.temp << "℃\n";
    }
};

int main() {
    Device myDevice(68);
    Monitor monitor;
    monitor.checkStatus(myDevice);
    return 0;
}

运行结果如下。

内部温度:68℃

分析:

friend class Monitor; 这句代码写在 Device 类内部,表示将监控器类 Monitor 声明为 Device 的友元类。

这样一来,虽然 temp 是 Device 的私有成员变量,不允许普通外部代码随意读写,但配套的 Monitor 类可以在 checkStatus() 中直接通过 dev.temp 读取设备内部的温度值。

友元的特点与使用原则

在使用 friend 处理类与类之间的协作时,有以下 3 条明确的边界规则,小伙伴们不要搞混了:

  • 友元关系是单向的:类 A 把类 B 声明为友元,只代表类 B 能访问类 A 的私有成员,并不代表类 A 也能反过来访问类 B 的私有成员。
  • 友元关系不能传递:假设类 A 把类 B 声明为友元,而类 B 又把类 C 声明为友元,这并不代表类 C 可以直接访问类 A 的私有成员。
  • 友元关系不能被继承:当一个类发生继承时,父类的友元并不会自动成为子类的友元。

比如在示例 2 中,Device 把 Monitor 声明成了友元类。如果 Device 反过来尝试直接读取 Monitor 的私有成员,就会因为友元的单向性而直接编译报错。下面我们通过一个例子来验证一下。

示例 3:友元关系的单向性

#include <iostream>

class Monitor {
private:
    int version = 2;                       // 监控器的私有成员
};

class Device {
private:
    int temp = 68;
    friend class Monitor;                  // 只授权给了 Monitor

public:
    void readMonitor(const Monitor& m) {
        std::cout << m.version << '\n';    // 错误:无权访问
    }
};

int main() {
    Monitor monitor;
    Device dev;
    dev.readMonitor(monitor);
    return 0;
}

运行结果如下。

(报错)error: 'int Monitor::version' is private within this context

分析:

虽然 Device 类内部写了 friend class Monitor;,但这只表示 Device 授权 Monitor 访问自己的私有成员,Monitor 并没有把门禁卡发给 Device。因此,当 readMonitor() 尝试读取 m.version 时,编译会直接报错,提示该成员属于私有成员(is private)。

在实际开发中,建议大家克制使用 friend,不要滥用,因为每增加一个友元都会削弱类的封装性。平时如果通过公开的 getXXX() 成员函数就能拿到数据,我们优先使用普通成员函数即可。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号