友元函数="被某个类点名放行的普通函数"。它不是类的成员(没有 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
读结果四句话:
add(x, y)和equal(x, y)都是普通函数的调用样子——不用x.add(y),两个参数地位对等。- 它们都能直接读
a.real、a.imag——因为被Complex点名放行。 add的返回值又可以直接喂给add(add(add(x, y), Complex(2,2)))——因为返回的是Complex对象,能继续参与运算。Complex(2, 2)这种临时对象可以在调用处直接造出来用——构造函数在起作用(10.2)。
30 秒实验:把两个 friend 声明都注释掉,重新编译。你会看到 add 和 equal 里每一处 a.real、b.imag 都报错(is private within this context)——然后你会面临选择:是加四个 getReal/getImag(开门给所有人),还是加两行 friend(只给这两个函数开门)。这个选择就是友元存在的全部理由。
五、常见误区
- "友元函数写实现时要加
类名::"——绝对不要。友元函数不是成员,加了::就变成成员函数,友元声明和它对不上。报错通常是"找不到定义"或"参数不匹配"。 - "友元函数能用
this"——不能。它没有this(不是成员)。要用哪个对象,就把它当参数传进来。 - "友元函数必须在第一个朋友类里声明"——不是。每个想放行它的类,各自在自己的类里写一行
friend——两边都写,它才能同时进两扇门。 - "友元函数比
get/set差,因为它破坏了封装"——看情况。为了一个外部需求开四个get(等于公开),比给它一个精确的friend更破坏封装。判据还是那句:影响面谁更小? - "一个函数只能是"这个类的朋友",不能也是"那个类的朋友"——可以,而且这正是它的核心价值(第二节的
bridge)。这是成员函数做不到的事。
自测三问(点开对答案)
1. 友元函数的实现里要不要写 `类名::`?为什么?——**不要**。**友元函数不是类的成员**,写 `类名::` 就把它变成成员函数了(和友元声明冲突)。**它调用起来也和普通函数一样:`add(x, y)`。** 2. 友元函数和成员函数最本质的差别是什么?——**成员函数有 `this`**(属于某个对象,用对象来调);**友元函数没有 `this`**(是外人,参数都得自己传)。所以**友元函数能同时访问多个类**——这是它的独门本事,成员函数做不到。 3. 为什么"两个对象之间的运算"(如相加、比距离)适合用友元函数?——因为**这个动作不属于某一个对象,而属于"两者之间"**。用友元函数,**两个参数地位对等**(`add(a, b)`),比成员函数的 `a.add(b)` 更自然、语义更准。**这也是运算符重载用友元函数的原因。**- ☐ 10.6 命名空间(下一节)