🔷 第十章 类和对象
📄 一句话概括

友元="你被允许进我家"的通行证。一个类可以指定 某个函数 或 某个类 做自己的"朋友",朋友就能访问它自己的私有成员。这是封装的"官方后门"——有它,两个联系紧密的类才能高效配合(最典型的场景是运算符重载,比如让 cout << 对象 能直接打印)。但后门开多了,墙就形同虚设——所以友元的原则是:能不开就不开,开了要写清理由。

📝 本节和前面几节的关系

10.1~10.4 讲的全是"怎么把数据藏严实"(封装)。本节第一次讲"怎么合法地翻墙"——它没有推翻封装,而是给封装加了一个受控的例外。用到的旧知识:10.4.1 的访问权限(友元就是"权限的例外名单")、8.5 的共用体配 tag(其实也是"两个东西必须亲密协作"的例子,只是那时用的是公开成员)。

一、为什么需要"后门"

先想一个真实需求:我们想让下面的写法和 cout << 5 一样自然:

Point p(3, 4);
cout << p << endl;          // 希望输出 (3, 4)

问题来了:这个 << 是个函数(运算符重载,后面章节细讲),它的两个参数是 cout 和 p。要打印 p 的内容,它必须读 p 的私有成员 x、y——可它是外人(不是 Point 的成员函数),按 10.4.1 的规矩,它碰不到。

怎么办?三条路:

方案 代价
给 Point 加一堆 getX()、getY() 把数据变相公开了(10.1.1 说过:门开太多=没墙)
把 x、y 改成 public 彻底放弃封装
让这个函数成为"友元" 只给它一个人开门——这就是友元的用处

一句话:友元是"点名放行"——不是把墙拆了,而是给某个人发张卡。

二、友元有三种

种类 谁获得了访问权 写法(写在类里)
友元函数 一个普通函数 friend void show(const Point &p);
友元类 另一个类(它的所有成员函数都能访问) friend class Inspector;
友元成员函数 另一个类里的某一个函数 friend void Inspector::check(const Point &p);

"友元成员函数"是前两种的折中:不给整个类开门,只给它里的某个函数开门(精度最高,写法也最麻烦)。前两种最常用。

三、友元的四条规矩

规矩 说明
① 单向的 A 把 B 当朋友,不代表 B 把 A 当朋友。"我让你进我家"不等于"你能让我进你家"。
② 不传递 A 是 B 的朋友、B 是 C 的朋友,A 和 C 之间没有任何关系
③ 不是成员 友元不是这个类的成员——它没有 this、不属于这个类,只是"有访问权"
④ 声明位置无所谓 friend 声明写在 private 还是 public 里效果一样——它不是成员,不受访问修饰符管辖

第 ④ 条要特别说明:友元声明只是"登记一份名单",写在哪儿都行。为了读代码方便,惯例是写在类的最前面或最后面,并用注释说明理由。

四、友元是不是"破坏封装"

这是最值得想清楚的一个问题。答案分两面:

立场 理由
是的,它有代价 它绕过了"数据只能从公开方法进"这条规矩。后门多了,封装的保护就没了——而且友元的改动不受那个类的接口约束(改数据的地方多了一处)
但它有存在的必要 有些需求(运算符重载最典型)绕不过去。硬用 get/set 顶上,反而要开一堆门,比友元更糟

实用判据: ① 能不用就不用——先用公开方法想想能不能解决。 ② 只在"两个东西本来就该亲密协作"时用——比如"点"和"打印点的函数"、"链表"和"迭代器"。 ③ 用了要写清理由——在 friend 那行加个注释,告诉后来的人"为什么这里必须开后门"。

五、本节路线

小节 内容
10.5.1 友元概述 完整规则:怎么写、四种格式、常见误区、哪些场景真的需要
10.5.2 友元类 friend class X;——整个类都能进;含"两个类互相做朋友"的合法写法
10.5.3 友元函数 friend 返回类型 f(...);;含实例04;最典型的实战场景

六、动手 Demo:先感受一下"没有友元时的别扭"

需求:写一个函数 printPoint,打印 Point 的内容。

#include <iostream>
using namespace std;

class Point {
private:
    int x, y;
public:
    Point(int a, int b) : x(a), y(b) { }
    // 只给"读"开门(不加友元,先试试这样写)
    int getX() const { return x; }
    int getY() const { return y; }
};

// 外人:只能靠 getX/getY 间接读
void printPoint(const Point &p) {
    cout << "(" << p.getX() << ", " << p.getY() << ")" << endl;
}

int main() {
    Point p(3, 4);
    printPoint(p);            // (3, 4)
    return 0;
}

它能跑,输出也对。 那为什么还想要友元?——因为这是"为了一个外部函数,专门开两个门"。

如果 Point 有十个成员呢?就要开十个 getX()——这就是"门开得跟没墙一样"(9.2.3 批评过的做法)。

再看有友元的版本(10.5.3 会详细讲):

class Point {
private:
    int x, y;
public:
    Point(int a, int b) : x(a), y(b) { }

    friend void printPoint(const Point &p);      // ★ 只给这一个函数开门
};

void printPoint(const Point &p) {                // 现在可以直接读私有成员
    cout << "(" << p.x << ", " << p.y << ")" << endl;
}

对比:

用 get 函数 用友元
要不要为外部需求开新方法 要(每加一个成员就得开一个) 不用
外部能拿到数据吗 能(等于公开了) 只能通过那个被点名的函数
影响范围 所有能拿到对象的人 只有这一个函数

所以友元不是"更松",有时反而是"更紧"——它把"给所有人开门"换成了"只给一个人开门"。

30 秒实验:在上面的"有友元版本"里,再加一个函数 void hack(Point &p) { p.x = 0; }(不声明友元),编译试试。你会看到熟悉的 private within this context——说明"只有被点名的那个函数"才进得来。 这就是"点名放行"的意思。

七、常见误区

  1. "友元让类失去了封装"——不对。它只对特指的"朋友"开放,其他所有人依然被挡在墙外。友元是"有名单的后门",不是"拆墙"。
  2. "成了友元就自动变成成员了"——不是。友元依然是外人——它没有 this、不属于这个类,只是被授予了访问私有成员的权限。
  3. "友元关系是双向的"——单向。"我信任你"不等于"你信任我"。 要互相访问,两边都得声明(10.5.2 会给出合法写法)。
  4. "友元关系能传下去"——不能。A 的朋友是 B、B 的朋友是 C,A 和 C 毫无关系。(这条规矩的存在,让友元的影响面可控。)
  5. "有了友元,就不需要 get/set 了"——不冲突。它们服务不同对象:get/set 面向所有使用者(设计好的公开接口);友元面向特定的协作方。该设计公开接口的地方,还是要好好设计接口。
自测三问(点开对答案) 1. 友元一共几种?各给谁开门?——三种:**友元函数**(给一个普通函数开门)、**友元类**(给另一个类的**所有**成员函数开门)、**友元成员函数**(只给另一个类里的某一个函数开门)。 2. 友元关系是单向的还是双向的?能传下去吗?——**单向**(我让你进我家 ≠ 你能让我进你家,要互相就得两边都声明);**不能传递**(A→B、B→C 不代表 A→C)。 3. 为什么说"友元有时反而更安全"?——因为不用为了某个外部需求**开一堆 `get/set`**(那等于把数据变相公开给所有人)。**友元只是把权限给到点名的那个函数**,影响面更小。

相关笔记