在 C++ 中,类的 private(私有)成员默认只能在当前类的内部访问,外部函数或其他类都无法直接读写它。但在少数场景下,某个外部函数或另一个类与当前类的关系非常紧密,确实需要直接访问它的私有数据。
为了处理这种特殊的协作关系,C++ 提供了 friend(友元)关键字,它的英文原意就是 “朋友”。
你可以把 friend 想象成一张 “点名发放的专属门禁卡”:private 这扇大门依然关着,只是当前类明确点名让某个外部函数或外部类进来,没有被点名的外部代码依然进不去,如下图所示。

在 C++ 中,友元主要包括以下 2 种常见形式:
- 友元函数:授权某一个特定的外部普通函数,允许它直接访问当前类的
private和protected成员。 - 友元类:授权另一个类,允许该类的所有成员函数直接访问当前类的
private和protected成员。
C++ 友元函数
在 C++ 中,如果我们在类的内部使用 friend 关键字声明一个普通函数,那么这个函数就成了当前类的 “友元函数”,可以直接访问该类的私有成员和受保护成员。
语法:
class 类名 {
private:
// 私有成员
friend 返回值类型 函数名(参数列表);
};说明:
初学友元函数时,很多小伙伴看到函数声明写在类的花括号里面,容易误以为它变成了类的成员函数。实际上并非如此,友元函数内部并没有 this 指针,我们在使用时要特别分清以下 2 点:
- 友元函数不是类的成员函数:它本质上依然是一个独立的类外普通函数,类里面的
friend语句只是给它发了一张 “访问通行证”。 - 按普通函数的方式定义和调用:我们在类的外部定义该函数时,不需要再加
friend关键字,也不需要加类名::前缀。在main()函数中调用时,我们直接写函数名(实参)即可,不能写成对象名.函数名()。
下面我们通过一个比较两个长方形盒子 Box 面积大小的例子,来看看友元函数的具体用法。
示例 1:使用友元函数
#include <iostream>
class Box {
private:
int width;
int height;
public:
Box(int w, int h) : width(w), height(h) {}
// 声明 isLarger 为友元函数
friend bool isLarger(const Box& b1, const Box& b2);
};
// 类外定义普通函数 isLarger
bool isLarger(const Box& b1, const Box& b2) {
return b1.width * b1.height > b2.width * b2.height;
}
int main() {
Box box1(4, 5); // 面积为 20
Box box2(3, 6); // 面积为 18
if (isLarger(box1, box2)) {
std::cout << "box1 的面积更大\n";
} else {
std::cout << "box2 的面积更大或相等\n";
}
return 0;
}运行结果如下。
box1 的面积更大分析:
在这个例子中,我们在 Box 类内部使用 friend 将外部普通函数 isLarger() 声明为友元函数。虽然 width 和 height 都是私有成员,但获得授权后,isLarger() 在函数体内就可以直接读取 b1.width 和 b2.width。
同时,这里我们把形参写成了常引用 const Box&,既能省去复制整个对象的开销,又能保证函数内部只读取数据而不修改原对象。在调用友元函数时,小伙伴们要注意,下面这种把它当成成员函数来调用的写法是错误的。
box1.isLarger(box2);当然了,对于比较两个盒子的面积,我们平时直接写一个公开的 getArea() 成员函数就够用了,这里只是为了方便大家看清友元函数的语法。在实际开发中,友元函数最常见的用场是配合下一节要学习的运算符重载(如重载 << 输出运算符)。
提示 friend 声明本身不受访问修饰符的影响。无论我们把 friend 声明写在类的 private、protected 还是 public 区域,授权效果都完全一样。
C++ 友元类
在 C++ 中,除了单独给某一个函数授权之外,我们还可以把整个外部类声明为当前类的 “友元类”。一旦类 A 把类 B 声明为友元类,类 B 中的所有成员函数就都可以直接访问类 A 的私有成员和受保护成员。
语法:
class 类名A {
private:
// 类 A 的私有成员
friend class 类名B;
};说明:
书写时我们要看清授权方向:在类 A 里面写 friend class 类名B;,表示 “类 A 主动把门禁卡发给类 B”,从此类 B 可以访问类 A 的私有数据。哪怕 类名B 的完整定义写在类 A 后面,这句声明也是合法的,编译器会自动识别出 类名B 是一个类。
示例 2:使用友元类
#include <iostream>
class Device {
private:
int temp; // 内部温度
public:
Device(int t) : temp(t) {}
// 声明 Monitor 为友元类
friend class Monitor;
};
class Monitor {
public:
void checkStatus(const Device& dev) const {
// 直接读取 Device 的私有成员
std::cout << "内部温度:" << dev.temp << "℃\n";
}
};
int main() {
Device myDevice(68);
Monitor monitor;
monitor.checkStatus(myDevice);
return 0;
}运行结果如下。
内部温度:68℃分析:
friend class Monitor; 这句代码写在 Device 类内部,表示将监控器类 Monitor 声明为 Device 的友元类。
这样一来,虽然 temp 是 Device 的私有成员变量,不允许普通外部代码随意读写,但配套的 Monitor 类可以在 checkStatus() 中直接通过 dev.temp 读取设备内部的温度值。
友元的特点与使用原则
在使用 friend 处理类与类之间的协作时,有以下 3 条明确的边界规则,小伙伴们不要搞混了:
- 友元关系是单向的:类 A 把类 B 声明为友元,只代表类 B 能访问类 A 的私有成员,并不代表类 A 也能反过来访问类 B 的私有成员。
- 友元关系不能传递:假设类 A 把类 B 声明为友元,而类 B 又把类 C 声明为友元,这并不代表类 C 可以直接访问类 A 的私有成员。
- 友元关系不能被继承:当一个类发生继承时,父类的友元并不会自动成为子类的友元。
比如在示例 2 中,Device 把 Monitor 声明成了友元类。如果 Device 反过来尝试直接读取 Monitor 的私有成员,就会因为友元的单向性而直接编译报错。下面我们通过一个例子来验证一下。
示例 3:友元关系的单向性
#include <iostream>
class Monitor {
private:
int version = 2; // 监控器的私有成员
};
class Device {
private:
int temp = 68;
friend class Monitor; // 只授权给了 Monitor
public:
void readMonitor(const Monitor& m) {
std::cout << m.version << '\n'; // 错误:无权访问
}
};
int main() {
Monitor monitor;
Device dev;
dev.readMonitor(monitor);
return 0;
}运行结果如下。
(报错)error: 'int Monitor::version' is private within this context分析:
虽然 Device 类内部写了 friend class Monitor;,但这只表示 Device 授权 Monitor 访问自己的私有成员,Monitor 并没有把门禁卡发给 Device。因此,当 readMonitor() 尝试读取 m.version 时,编译会直接报错,提示该成员属于私有成员(is private)。
在实际开发中,建议大家克制使用 friend,不要滥用,因为每增加一个友元都会削弱类的封装性。平时如果通过公开的 getXXX() 成员函数就能拿到数据,我们优先使用普通成员函数即可。
