友元="你被允许进我家"的通行证。一个类可以指定 某个函数 或 某个类 做自己的"朋友",朋友就能访问它自己的私有成员。这是封装的"官方后门"——有它,两个联系紧密的类才能高效配合(最典型的场景是运算符重载,比如让 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——说明"只有被点名的那个函数"才进得来。 这就是"点名放行"的意思。
七、常见误区
- "友元让类失去了封装"——不对。它只对特指的"朋友"开放,其他所有人依然被挡在墙外。友元是"有名单的后门",不是"拆墙"。
- "成了友元就自动变成成员了"——不是。友元依然是外人——它没有
this、不属于这个类,只是被授予了访问私有成员的权限。 - "友元关系是双向的"——单向。"我信任你"不等于"你信任我"。 要互相访问,两边都得声明(10.5.2 会给出合法写法)。
- "友元关系能传下去"——不能。A 的朋友是 B、B 的朋友是 C,A 和 C 毫无关系。(这条规矩的存在,让友元的影响面可控。)
- "有了友元,就不需要
get/set了"——不冲突。它们服务不同对象:get/set面向所有使用者(设计好的公开接口);友元面向特定的协作方。该设计公开接口的地方,还是要好好设计接口。
自测三问(点开对答案)
1. 友元一共几种?各给谁开门?——三种:**友元函数**(给一个普通函数开门)、**友元类**(给另一个类的**所有**成员函数开门)、**友元成员函数**(只给另一个类里的某一个函数开门)。 2. 友元关系是单向的还是双向的?能传下去吗?——**单向**(我让你进我家 ≠ 你能让我进你家,要互相就得两边都声明);**不能传递**(A→B、B→C 不代表 A→C)。 3. 为什么说"友元有时反而更安全"?——因为不用为了某个外部需求**开一堆 `get/set`**(那等于把数据变相公开给所有人)。**友元只是把权限给到点名的那个函数**,影响面更小。- ☐ 10.5.1 友元概述(下一节)
相关笔记
- 局部类 —— 上一节:函数里定义的类
- 友元概述 —— 下一节:友元的完整规则
- 面向对象的特点 —— 封装的边界在哪(友元是它的受控例外)
- 访问类成员 —— 被友元打破的正是这条访问规则
- 🔷 C++基础 · 索引 —— 全课路线图