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

友元类="把整个类当朋友"——这个类的所有成员函数都能访问本类的私有成员。写法是在本类里写一句 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

读结果三句话:

  1. Manager 的四个函数都直接读写 w.box、w.total——一句 friend class Manager; 让整套操作都通了。
  2. 补货按"能装多少"自动收敛(23 件先装满前两个货位 20,再放 3 到第三个)——规则写在管理员手里,仓库只管存。
  3. getTotal() 依然保留——友元不排斥公开接口。外面的人想知道总数,走公开接口;内部协作的事,走友元。

30 秒实验:把 friend class Manager; 那行注释掉,编译试试。你会看到 Manager 里每个碰 w.box、w.total 的函数都报错(is private within this context)——一行注释,四条报错。这直观说明了"友元类是一次性放行一整队"。

五、常见误区

  1. "friend class B; 让 B 变成 A 的成员"——不是。B 依然是独立的类,只是获得了访问 A 私有成员的权限。它没有 this 指向 A,也不占 A 的内存。
  2. "A 放行 B,B 也自动放行 A"——不会。单向的。要互相访问,两边都要写 friend(第二节给了正确写法)。
  3. "A 放行 B,B 放行 C,所以 C 能访问 A"——不能。友元不传递。
  4. "友元类比友元函数好,因为省事"——省事不等于好。权限给得越宽,封装越虚。能用"友元成员函数"(只放行一个函数)就用它;整个类都该有权限时,才用 friend class。
  5. "写了 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. 什么时候该用友元类,什么时候该用友元成员函数?——**"整个类都是干这件事的"**(如"仓库"与"仓库管理员")→ 用**友元类**;**"只有一两个函数需要"** → 用**友元成员函数**(精度更高)。**能精确就别图省事。**

相关笔记