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

友元函数="被某个类点名放行的普通函数"。它不是类的成员(没有 this、不属于任何对象),调用方式和普通函数完全一样,但可以访问那个类的私有成员。它有一个独一无二的本事:可以被多个类同时声明为友元——于是它成了"跨越两个类"的最佳人选(比如计算"两个点之间的距离"、相加两个"复数")。实例04 就演示这类用法。

📝 本节和前面几节的关系

10.5.2 讲"整个类放行",本节讲"放行一个函数"——精度更高、更常用。这里会大量复用:6.1.1 的函数定义(友元函数写起来就是普通函数)、10.4.4 的 this(友元函数没有 this,所以参数得写全)、7.8.2 的 const & 传参(友元函数接大对象的标准姿势)。后面学运算符重载时,<< 的写法就是本节内容的直接应用。

一、写法与三个特点

class Point {
private:
    int x, y;

public:
    Point(int a = 0, int b = 0) : x(a), y(b) { }

    friend void printPoint(const Point &p);      // ★ 在这一行"点名"
};

// 类外写实现——注意:**不带 `Point::`**
void printPoint(const Point &p) {
    cout << "(" << p.x << ", " << p.y << ")" << endl;   // 能直接读私有成员
}

三个特点:

特点 说明
不是成员 写实现时不要带 Point::!带了就变成"成员函数",反而和友元声明冲突
没有 this 所以要用哪个对象,必须当参数传进来(和静态成员函数一样,10.4.3)
调用像普通函数 printPoint(p);——不用写成 p.printPoint()

第二点特别容易反直觉:它明明能读 p.x,却必须先把 p 传进来。因为"能进这扇门"和"知道给谁办事"是两件事——通行证给了你,但你还得自己走进指定的那间房。

二、它的独门本事:一个函数、多个类的朋友

这是友元函数最独特的价值——友元类做不到,成员函数也做不到。

场景:想算"两个点之间的距离"——这个函数同时需要 p1 和 p2 的内部数据。

写成什么 有问题吗
Point 的成员函数(p1.distance(p2)) 勉强能写,但语义别扭("距离"不属于某一个点,而是两个点之间的事)
借助 getX/getY 要开四个接口(更糟)
友元函数 最自然:distance(p1, p2)——它是"两个点"的事,就该站在外面看

写法:

class Point {
private:
    int x, y;
public:
    Point(int a = 0, int b = 0) : x(a), y(b) { }
    friend double distance(const Point &a, const Point &b);     // 点名放行
};

double distance(const Point &a, const Point &b) {              // 两个点都传进来
    int dx = a.x - b.x;
    int dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);                            // 需 <cmath>
}

更关键的用法:一个函数同时是两个类的朋友

class B;                                   // 前向声明

class A {
    friend void bridge(const A &a, const B &b);    // A 放行
private:
    int va;
public:
    A(int v) : va(v) { }
};

class B {
    friend void bridge(const A &a, const B &b);    // ★ B 也放行(两边都写)
private:
    int vb;
public:
    B(int v) : vb(v) { }
};

void bridge(const A &a, const B &b) {      // 它同时看得到 a.va 和 b.vb
    cout << a.va + b.vb << endl;
}

这是友元函数"不可替代"的地方:成员函数永远只能"自己一个类"的视角(要有 this),只有友元函数能同时站在两个类的内部。 这就是为什么运算符重载(如 a + b,两个操作数来自同一个类)和"组合两个对象"的操作几乎都是友元函数。

三、什么时候用它

场景 为什么适合友元函数
运算符重载 operator<<、operator+ 等天生是函数,且常需读两个对象——后面章节的主角
计算两个对象之间的关系 distance(a, b)、isSame(a, b)——站在这两个对象外面看最自然
跨越两个类的操作 上面 bridge 那种——只有它能同时进两扇门
工具函数 需要一个"非成员但要看内部"的辅助函数时

四、实例04:定义友元函数

目标:写一个 Complex(复数)类,用友元函数实现"两个复数相加"。

为什么不用成员函数?add(a, b) 这个动作属于"两个复数之间",不属于某一个——用友元函数语义最准,而且两个参数的写法对称(成员函数会写成 a.add(b),看着不对称)。

#include <iostream>
using namespace std;

class Complex {
private:
    double real;                     // 实部
    double imag;                     // 虚部

public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) { }

    void show(const char *who) const {
        cout << who << " = " << real;
        if (imag >= 0) cout << " + " << imag << "i" << endl;
        else           cout << " - " << -imag << "i" << endl;
    }

    // ★ 实例04:声明两个友元函数
    friend Complex add(const Complex &a, const Complex &b);   // 相加
    friend bool    equal(const Complex &a, const Complex &b); // 相等判断
};

// ── 友元函数实现:不带 Complex::,两个对象都当参数传进来 ──
Complex add(const Complex &a, const Complex &b) {
    return Complex(a.real + b.real, a.imag + b.imag);        // 能直接读私有成员
}

bool equal(const Complex &a, const Complex &b) {
    return (a.real == b.real) && (a.imag == b.imag);
}

int main() {
    Complex x(3, 4);
    Complex y(1, -2);

    x.show("x");
    y.show("y");

    Complex z = add(x, y);
    z.show("x + y");

    cout << "x 等于 y 吗?" << (equal(x, y) ? "是" : "否") << endl;
    cout << "x 等于 x 吗?" << (equal(x, x) ? "是" : "否") << endl;

    // 也能链式套起来用
    Complex w = add(add(x, y), Complex(2, 2));
    w.show("x + y + (2+2i)");
    return 0;
}

看到什么算成功:

x = 3 + 4i
y = 1 - 2i
x + y = 4 + 2i
x 等于 y 吗?否
x 等于 x 吗?是
x + y + (2+2i) = 6 + 4i

读结果四句话:

  1. add(x, y) 和 equal(x, y) 都是普通函数的调用样子——不用 x.add(y),两个参数地位对等。
  2. 它们都能直接读 a.real、a.imag——因为被 Complex 点名放行。
  3. add 的返回值又可以直接喂给 add(add(add(x, y), Complex(2,2)))——因为返回的是 Complex 对象,能继续参与运算。
  4. Complex(2, 2) 这种临时对象可以在调用处直接造出来用——构造函数在起作用(10.2)。

30 秒实验:把两个 friend 声明都注释掉,重新编译。你会看到 add 和 equal 里每一处 a.real、b.imag 都报错(is private within this context)——然后你会面临选择:是加四个 getReal/getImag(开门给所有人),还是加两行 friend(只给这两个函数开门)。这个选择就是友元存在的全部理由。

五、常见误区

  1. "友元函数写实现时要加 类名::"——绝对不要。友元函数不是成员,加了 :: 就变成成员函数,友元声明和它对不上。报错通常是"找不到定义"或"参数不匹配"。
  2. "友元函数能用 this"——不能。它没有 this(不是成员)。要用哪个对象,就把它当参数传进来。
  3. "友元函数必须在第一个朋友类里声明"——不是。每个想放行它的类,各自在自己的类里写一行 friend——两边都写,它才能同时进两扇门。
  4. "友元函数比 get/set 差,因为它破坏了封装"——看情况。为了一个外部需求开四个 get(等于公开),比给它一个精确的 friend 更破坏封装。判据还是那句:影响面谁更小?
  5. "一个函数只能是"这个类的朋友",不能也是"那个类的朋友"——可以,而且这正是它的核心价值(第二节的 bridge)。这是成员函数做不到的事。
自测三问(点开对答案) 1. 友元函数的实现里要不要写 `类名::`?为什么?——**不要**。**友元函数不是类的成员**,写 `类名::` 就把它变成成员函数了(和友元声明冲突)。**它调用起来也和普通函数一样:`add(x, y)`。** 2. 友元函数和成员函数最本质的差别是什么?——**成员函数有 `this`**(属于某个对象,用对象来调);**友元函数没有 `this`**(是外人,参数都得自己传)。所以**友元函数能同时访问多个类**——这是它的独门本事,成员函数做不到。 3. 为什么"两个对象之间的运算"(如相加、比距离)适合用友元函数?——因为**这个动作不属于某一个对象,而属于"两者之间"**。用友元函数,**两个参数地位对等**(`add(a, b)`),比成员函数的 `a.add(b)` 更自然、语义更准。**这也是运算符重载用友元函数的原因。**

相关笔记