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

友元的写法就一句:在类里用 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 没被点名
}

读这个例子抓住两件事:

  1. friend 声明都写在 Point 里面——因为是"被访问者"在发通行证。
  2. ②友元类和③友元成员函数是"粗"和"细"两种给法"——要整个类的所有函数都能进,用 ②;只给一个函数,用 ③。③ 更精确,冲突面更小。

二、四条"容易记错"的细节

细节 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)。试一次就记住了。

五、常见误区

  1. "friend 声明写在 public 里,友元才能在 main 里调用"——错。友元声明的位置和访问权限无关。函数能不能在 main 里调用,看的是那个函数自己的访问性(全局函数当然哪儿都能调)。
  2. "写了 friend 声明就等于定义了函数"——不是。friend 那行只是声明 + 授权,函数体还得自己写(6.1.2 的规矩)。
  3. "friend class X; 和 friend void X::f(...); 效果一样"——不一样。前者给 X 的所有成员函数开门,后者只给一个函数开门。精度不同,能用后者就别用前者。
  4. "友元声明可以写在朋友那边"——不行。通行证只能由"被访问的类"发。你在自己家里宣布"我是邻居家的朋友",没有人理你。
  5. "friend 关键字后面什么限定都能写"——不能。标准写法不带 public/static/virtual 这类修饰,就是 friend + 一个函数/类的声明。写了多余的关键字,编译器会报语法错误。
自测三问(点开对答案) 1. `friend class X;` 和 `friend void X::f();` 有什么区别?——**前者**让 `X` 的**所有成员函数**都能访问本类的私有成员;**后者只让 `X::f` 这一个函数**能访问。**后者精度更高,能用就用后者。** 2. `friend` 声明写在 `private` 区和写在 `public` 区,效果一样吗?——**一样**。因为**友元声明不是成员声明**,它只是一份"名单",**不受访问修饰符管辖**。 3. 什么时候该用友元,什么时候不该用?——**该用**:需求**天生绕不过封装**时(**运算符重载**最典型),或两个类**必须紧密协作**时。**不该用**:只是想"读一个值/改一个值"(那应该设计 `get`/`set` 接口),或者只是"暂时图方便"。

相关笔记