友元类="把整个类当朋友"——这个类的所有成员函数都能访问本类的私有成员。写法是在本类里写一句 friend class 友元类名;。它的特点是"一次给一整队人开门"——方便,但比"只给一个函数开门"(10.5.1 的友元成员函数)精度低。单向、不传递这两条规矩在这里尤其要注意:要让两个类互相访问,必须"两边各声明一次"。
10.5.1 讲了四种友元格式,本节专门把"友元类"这一种说透。它和 10.4.5 嵌套类有一处容易混:嵌套类管"名字的定义位置",友元类管"访问权限"——两件事。另外,本节会用到 10.1.2 的前向声明(写"互相做朋友"时必须用)。
一、基本写法
class Inspector; // 前向声明(10.1.2)
class Point {
private:
int x, y;
public:
Point(int a = 0, int b = 0) : x(a), y(b) { }
friend class Inspector; // ★ 整队放行:Inspector 的所有成员函数都能进
};
class Inspector {
public:
void show(const Point &p) {
cout << "(" << p.x << ", " << p.y << ")" << endl; // ✅ 能读(因为整队被放行)
}
void hack(Point &p) {
p.x = 0; // ✅ 也能改(整队放行,包括"改"的权限)
p.y = 0;
}
};
| 对比项 | friend class Inspector; |
friend void Inspector::check(const Point &); |
|---|---|---|
| 放行范围 | Inspector 的全部成员函数 | 只有一个函数 |
| 写法复杂度 | 简单(一行,不需要先定义 Inspector) | 麻烦(Inspector 必须先定义完整) |
| 适用场合 | Inspector 整个类都该有权限时 | 只有个别函数需要时 |
取舍原则:能给"一个函数"就别给"一个类"。 权限越小,将来出问题的面越小。
friend class是"省事版",用之前先问一句:"这个类里的每个函数,都真的需要看我的内部吗?"
二、单向 + 不传递(两条硬规矩)
单向:我让你进 ≠ 你能让我进
class A {
friend class B; // B 能访问 A 的私有成员
private:
int secretA;
};
class B {
private:
int secretB;
};
// B 里的函数可以读 A::secretA ✅
// A 里的函数**不能**读 B::secretB ❌(A 没被 B 放行)
想互相访问,就必须各声明一次:
class B; // 前向声明
class A {
friend class B; // A 放行 B
private:
int secretA;
public:
void lookAtB(const B &b);
};
class B {
friend class A; // ★ B 也放行 A(两句都要写)
private:
int secretB;
public:
void lookAtA(const A &a);
};
注意这个坑:只写一句,只有一边能看。症状是"一边编译过、另一边报 private"——这个不对称的报错特别容易让人懵。
不传递:朋友的朋友不是朋友
class A { friend class B; };
class B { friend class C; };
// C **不能**访问 A 的私有成员!
这条规矩的存在让友元的影响面可控——不会因为你信任了一个人,你的所有秘密就顺着关系网流出去了。
三、为什么要"整个类"都放行
场景想一想:一个容器类,配一个"管理/检查"类。
class Warehouse { // 仓库
private:
int stock[100];
int count;
// ... 一堆内部细节
};
class WarehouseManager { // 仓库管理员:要做很多种操作
public:
void audit(const Warehouse &w); // 盘点
void refill(Warehouse &w); // 补货
void transfer(Warehouse &w); // 调拨
void report(const Warehouse &w); // 报表
// ... 还有十来个函数
};
每个函数都要读 stock 和 count——如果都声明成"友元成员函数",要写十几行(10.5.1);如果都靠 get/set,要开十几个接口(更糟)。
这时候 friend class WarehouseManager; 一句就够。 代价是"权限给宽了"——换来的是"写法简单、语义清楚"("管理员本来就该看仓库的全部细节")。
判据:"这个类整体都是干这件事的" → 用
friend class;"只有其中一两个函数需要" → 用友元成员函数。
四、动手 Demo:仓库与它的管理员
#include <iostream>
using namespace std;
class Warehouse; // 前向声明
class Manager {
public:
// ① 盘点:读私有数组
void audit(const Warehouse &w);
// ② 补货:改私有数组(需要完整定义,见下)
void refill(Warehouse &w, int amount);
// ③ 只汇报总数
void report(const Warehouse &w);
};
class Warehouse {
private:
int box[5]; // 5 个货位
int total;
public:
Warehouse() : total(0) {
for (int i = 0; i < 5; i++) box[i] = 0;
}
friend class Manager; // ★ 整队放行
// 一个公开接口,方便对照
int getTotal() const { return total; }
};
// ── 因为用了 p.box / w.total 这些私有成员,这几个实现放在 Warehouse 之后 ──
void Manager::audit(const Warehouse &w) {
cout << "盘点:";
for (int i = 0; i < 5; i++) cout << w.box[i] << " ";
cout << "| 合计 " << w.total << endl;
}
void Manager::refill(Warehouse &w, int amount) {
int space = 0;
for (int i = 0; i < 5; i++) space += (10 - w.box[i]); // 每个货位最多 10
if (amount > space) amount = space; // 装不下就按能装的算
for (int i = 0; i < 5 && amount > 0; i++) {
int put = (10 - w.box[i] < amount) ? (10 - w.box[i]) : amount;
w.box[i] += put;
amount -= put;
}
w.total = 0;
for (int i = 0; i < 5; i++) w.total += w.box[i];
cout << "补货完成,现在合计 " << w.total << endl;
}
void Manager::report(const Warehouse &w) {
cout << "报表:当前库存 " << w.total << " 件" << endl;
}
int main() {
Warehouse w;
Manager m;
m.audit(w);
m.refill(w, 23); // 每个货位上限 10,总共 5×10=50
m.audit(w);
m.report(w);
cout << "公开接口也能读到 total:" << w.getTotal() << endl;
return 0;
}
看到什么算成功:
盘点:0 0 0 0 0 | 合计 0
补货完成,现在合计 23
盘点:10 10 3 0 0 | 合计 23
报表:当前库存 23 件
公开接口也能读到 total:23
读结果三句话:
Manager的四个函数都直接读写w.box、w.total——一句friend class Manager;让整套操作都通了。- 补货按"能装多少"自动收敛(23 件先装满前两个货位 20,再放 3 到第三个)——规则写在管理员手里,仓库只管存。
getTotal()依然保留——友元不排斥公开接口。外面的人想知道总数,走公开接口;内部协作的事,走友元。
30 秒实验:把 friend class Manager; 那行注释掉,编译试试。你会看到 Manager 里每个碰 w.box、w.total 的函数都报错(is private within this context)——一行注释,四条报错。这直观说明了"友元类是一次性放行一整队"。
五、常见误区
- "
friend class B;让 B 变成 A 的成员"——不是。B 依然是独立的类,只是获得了访问 A 私有成员的权限。它没有this指向 A,也不占 A 的内存。 - "A 放行 B,B 也自动放行 A"——不会。单向的。要互相访问,两边都要写
friend(第二节给了正确写法)。 - "A 放行 B,B 放行 C,所以 C 能访问 A"——不能。友元不传递。
- "友元类比友元函数好,因为省事"——省事不等于好。权限给得越宽,封装越虚。能用"友元成员函数"(只放行一个函数)就用它;整个类都该有权限时,才用
friend class。 - "写了
friend class B;就得先完整定义 B"——不需要!这里只要前向声明就够了(class B;)。只有 "friend void B::f();"(友元成员函数)才必须先有 B 的完整定义(10.5.1 细节 4)。这是两者写法差别最大的地方,别搞反。
自测三问(点开对答案)
1. `friend class B;` 和 `friend void B::f();` 在写法要求上有什么不同?——**`friend class B;` 只需要 `B` 的前向声明**(`class B;`);**`friend void B::f();` 必须先有 `B` 的完整定义**(因为要写出 `B::f` 的参数表,编译器得知道 `f` 长什么样)。 2. 两个类要"互相访问",怎么写?需要注意什么?——**两边各写一次 `friend`**:A 里 `friend class B;`,B 里 `friend class A;`。**最容易犯的错是只写一边**——症状是"一边编译过、另一边报 `private`"。 3. 什么时候该用友元类,什么时候该用友元成员函数?——**"整个类都是干这件事的"**(如"仓库"与"仓库管理员")→ 用**友元类**;**"只有一两个函数需要"** → 用**友元成员函数**(精度更高)。**能精确就别图省事。**- ☐ 10.5.3 友元函数(下一节)
相关笔记
- 友元概述 —— 上一节:四种友元格式
- 友元函数 —— 下一节:含实例04
- 嵌套类 —— "定义在类里"与"有访问权"是两件事
- 类的声明与定义 —— 前向声明的用法
- 🔷 C++基础 · 索引 —— 全课路线图