友元的写法就一句:在类里用 friend 声明。四种格式记牢:友元函数friend 返回类型 函数名(参数);、友元类friend class 类名;、友元成员函数friend 返回类型 其他类::函数名(参数);、友元类模板(后面章节遇到)。三条容易记错的:friend 声明写在 private 或 public 里效果一样(它不是成员)、声明里不写 friend 关键字以外的任何限定、友元必须在"被访问者"的类里声明(不是在朋友自己那边写)。
10.5 导言讲了"为什么需要友元",本节把格式和规矩补全。这里会用到 10.1.2 的 ::(友元成员函数的写法里,必须写清"哪个类的哪个函数")、以及 2.1.3 的"先声明后使用"(友元声明和函数声明一样,都要"先让编译器看见")。
一、四种格式(对照表)
| 格式 | 写法 | 给谁开门 |
|---|---|---|
| 友元函数 | friend 返回类型 函数名(参数表); |
一个全局函数 |
| 友元类 | friend class 类名; |
另一个类的全部成员函数 |
| 友元成员函数 | friend 返回类型 类名::函数名(参数表); |
另一个类里的某一个函数 |
| 友元类模板 | friend class 模板名<参数>; |
模板类(后面学) |
一个完整的例子(三种一起写):
class Point; // 前向声明(10.1.2)
class Inspector { // 待会儿要当"友元类"
public:
void check(const Point &p); // 待会儿要当"友元成员函数"
void other(const Point &p); // 这个不打算给权限
};
void printPoint(const Point &p); // 待会儿要当"友元函数"
class Point {
private:
int x, y;
public:
Point(int a = 0, int b = 0) : x(a), y(b) { }
// ① 友元函数:给一个全局函数开门
friend void printPoint(const Point &p);
// ② 友元类:给整个 Inspector 开门(它所有成员函数都能进)
// —— 如果已经写了③,这条通常就不需要了(②是粗糙版、③是精确版)
// friend class Inspector;
// ③ 友元成员函数:只给 Inspector::check 开门(更精确)
friend void Inspector::check(const Point &p);
};
void printPoint(const Point &p) {
cout << "(" << p.x << ", " << p.y << ")" << endl; // 现在能直接读私有
}
void Inspector::check(const Point &p) {
cout << "检查通过:" << p.x << "," << p.y << endl; // 也能读
}
void Inspector::other(const Point &p) {
// cout << p.x; // ❌ 编译错误:other 没被点名
}
读这个例子抓住两件事:
friend声明都写在Point里面——因为是"被访问者"在发通行证。②友元类和③友元成员函数是"粗"和"细"两种给法"——要整个类的所有函数都能进,用 ②;只给一个函数,用 ③。③ 更精确,冲突面更小。
二、四条"容易记错"的细节
细节 1:friend 声明写在哪儿都一样
class Point {
friend void printPoint(const Point &p); // 写在开头(默认 private 区)
private:
int x, y;
public:
Point(int a = 0, int b = 0) : x(a), y(b) { }
};
效果和写在 public: 里完全相同。因为友元声明不是成员声明——它只是在名单上添一行。访问修饰符管的是成员,管不到名单。
惯例:写在类的最前面(含
class名字下面),或者写在最后加注释说明理由。别写在中间让人猜。
细节 2:必须在"被访问者"里声明
class Point { ... }; // 想让 printPoint 进来
// 必须在这里面写:
// friend void printPoint(const Point &p);
不能在 printPoint 那边写"我是 Point 的朋友"——通行证只能由房子的主人发。
细节 3:friend 声明不产生"函数定义"
class Point {
friend void printPoint(const Point &p); // 只是声明
};
// 函数本体还得自己写(写到类外)
void printPoint(const Point &p) { ... }
friend 那一行只是"授权 + 声明"——不写函数体。这就和 6.1.2 的"函数声明"一样:声明过的东西,定义还得给。
细节 4:先让编译器"看见"朋友
如果朋友是"另一个类的成员函数"或"另一个类",必须先有它的声明:
class Point; // ① 先声明 Point(后面要用到)
class Inspector { ... }; // ② 再定义 Inspector(里面用到 Point)
class Point { // ③ 最后定义 Point(里面写 friend)
friend void Inspector::check(const Point &p);
};
为什么这么绕:写 friend void Inspector::check(...) 时,编译器必须知道 Inspector 是什么、check 长什么样。少一步就是"不认识的类型名"。
看到"顺序问题"就想起 10.1.2 的前向声明:"用指针/引用"只要前向声明,"访问成员"才需要完整定义。
三、什么时候真的需要友元
| 场景 | 为什么绕不过去 |
|---|---|
运算符重载(<<、+、== 等) |
它们天生是"函数",不是成员——要读私有成员,只能当友元(这是最主要的用途,后面章节细讲) |
| 两个类必须紧密结合 | 比如容器类和它的迭代器、图形和它的渲染器——它们的分工决定了必须互看内部 |
| 工厂函数(创建一个对象并设置它的私有状态) | 有时候只在构造时用一次,不想为它开公开接口 |
| 单元测试 | 想直接检查类的内部状态(这个理由在工业界常被接受) |
反过来,什么时候不需要友元:
| 情况 | 更好的做法 |
|---|---|
| 只是想读一个值 | 加一个const 的 get 函数(10.4.1) |
| 只是想改一个值 | 加一个带校验的 set 函数(这本来就是接口该干的事) |
| 只是暂时图方便 | 别图。图一次方便,以后所有人都会来加"临时友元" |
判据一句话:"这个需求,用公开接口实现起来会不会很别扭?" 会(比如运算符重载)→ 友元;不会 → 设计接口。
四、动手 Demo:三种给法的精度对比
#include <iostream>
using namespace std;
class Point; // 前向声明
// ── 两个"想访问 Point 私有成员"的外部函数 ──
void printPoint(const Point &p); // 打算给友元函数权
void tryHack(const Point &p); // 不给权(对照组)
class Point {
private:
int x, y;
public:
Point(int a = 0, int b = 0) : x(a), y(b) { }
friend void printPoint(const Point &p); // ★ 点名放行
// 注意:tryHack 没有被点名
};
void printPoint(const Point &p) {
cout << "printPoint 读到了:" << p.x << ", " << p.y << endl;
}
void tryHack(const Point &p) {
cout << "tryHack 只能靠公开函数读:" << endl;
// cout << p.x; // ← 去掉注释:编译不过
}
int main() {
Point p(7, 9);
printPoint(p);
tryHack(p);
return 0;
}
看到什么算成功:
printPoint 读到了:7, 9
tryHack 只能靠公开函数读:
读结果:同一个类、同样的两个外部函数,一个能进、一个进不来——差别只在于"有没有被点名"。 这就是友元的精确性:它不改动墙,只给名单上的人发卡。
30 秒实验:把 friend void printPoint(const Point &p); 里那行挪到 private: 区(现在它在 public: 里)——重新编译,结果完全一样。因为 friend 声明不是成员,不受访问修饰符管辖(细节 1)。试一次就记住了。
五、常见误区
- "
friend声明写在public里,友元才能在main里调用"——错。友元声明的位置和访问权限无关。函数能不能在main里调用,看的是那个函数自己的访问性(全局函数当然哪儿都能调)。 - "写了
friend声明就等于定义了函数"——不是。friend那行只是声明 + 授权,函数体还得自己写(6.1.2 的规矩)。 - "
friend class X;和friend void X::f(...);效果一样"——不一样。前者给X的所有成员函数开门,后者只给一个函数开门。精度不同,能用后者就别用前者。 - "友元声明可以写在朋友那边"——不行。通行证只能由"被访问的类"发。你在自己家里宣布"我是邻居家的朋友",没有人理你。
- "
friend关键字后面什么限定都能写"——不能。标准写法不带public/static/virtual这类修饰,就是friend+ 一个函数/类的声明。写了多余的关键字,编译器会报语法错误。
自测三问(点开对答案)
1. `friend class X;` 和 `friend void X::f();` 有什么区别?——**前者**让 `X` 的**所有成员函数**都能访问本类的私有成员;**后者只让 `X::f` 这一个函数**能访问。**后者精度更高,能用就用后者。** 2. `friend` 声明写在 `private` 区和写在 `public` 区,效果一样吗?——**一样**。因为**友元声明不是成员声明**,它只是一份"名单",**不受访问修饰符管辖**。 3. 什么时候该用友元,什么时候不该用?——**该用**:需求**天生绕不过封装**时(**运算符重载**最典型),或两个类**必须紧密协作**时。**不该用**:只是想"读一个值/改一个值"(那应该设计 `get`/`set` 接口),或者只是"暂时图方便"。- ☐ 10.5.2 友元类(下一节)
相关笔记
- 友元 —— 上一节:为什么需要开一扇后门
- 友元类 —— 下一节:整个类都能进
- 类的声明与定义 —— 前向声明:写友元声明时常用的技巧
- 访问类成员 —— 被友元例外的访问权限规则
- 🔷 C++基础 · 索引 —— 全课路线图