23. 友元:友元函数、友元类、友元的优缺点
友元(friend)这个特性,在C++里一直是个争议话题。有人觉得它破坏了封装,有人觉得它必不可少。我个人觉得,它就像一把锋利的刀——用好了切菜顺手,用不好容易伤着自己。
说白了,友元就是让类外部的某个函数或某个类,能够访问本类的私有成员。正常情况下,私有成员只有类内部才能碰,但友元机制给了你一个「开后门」的权限。
为什么需要友元?
你想想看,有时候我们确实需要一些外部函数来操作类的内部数据。比如运算符重载,特别是双目运算符,像 operator<< 这种,它左边是 ostream,右边是你的类对象。这种情况下,你没法把它写成成员函数,因为左操作数不是你的类。
我在项目中遇到过这样一个场景:写一个矩阵类,需要重载 operator* 实现矩阵乘法。如果只写成员函数版本,那 matrix * 2 可以,但 2 * matrix 就不行。这时候友元函数就派上用场了。
友元函数
友元函数不是类的成员函数,但它可以访问类的私有成员。声明时在函数前面加个 friend 关键字就行。
class Matrix {
private:
int data[3][3];
public:
Matrix() { /* 初始化 */ }
// 友元函数声明
friend Matrix operator*(const Matrix& m, int scalar);
friend Matrix operator*(int scalar, const Matrix& m);
// 友元函数:打印矩阵
friend void printMatrix(const Matrix& m);
};
// 友元函数定义
Matrix operator*(const Matrix& m, int scalar) {
Matrix result;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
result.data[i][j] = m.data[i][j] * scalar;
return result;
}
Matrix operator*(int scalar, const Matrix& m) {
return m * scalar; // 复用上面的实现
}
void printMatrix(const Matrix& m) {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j)
std::cout << m.data[i][j] << ' ';
std::cout << '\n';
}
}
小提示:友元函数可以放在类的 public、private 或 protected 区域,效果都一样。我习惯放在 public 区域开头,这样一眼就能看到这个类有哪些「外部朋友」。
友元类
友元类就更直接了——整个类都是你的朋友,它的所有成员函数都能访问你的私有成员。
class Engine {
private:
int horsepower;
double displacement;
friend class Mechanic; // 声明 Mechanic 是友元类
public:
Engine(int hp, double disp) : horsepower(hp), displacement(disp) {}
};
class Mechanic {
public:
void tuneEngine(Engine& e) {
// 可以访问 Engine 的私有成员
e.horsepower += 50;
e.displacement *= 1.1;
}
void printSpecs(const Engine& e) {
std::cout << "马力: " << e.horsepower << '\n';
std::cout << "排量: " << e.displacement << '\n';
}
};
注意:友元关系是单向的,不能传递。A 是 B 的友元,B 是 C 的友元,不代表 A 能访问 C 的私有成员。我曾经在代码评审里看到有人踩了这个坑,以为友元能像继承一样传递下去。
友元的优缺点
| 优点 | 缺点 |
|---|---|
| 方便实现某些特定功能(如运算符重载) | 破坏了封装性,增加了耦合 |
| 提高性能,避免频繁调用 getter/setter | 维护困难,修改一个类的私有成员可能影响多个友元 |
| 让两个紧密相关的类可以协作 | 友元关系不能继承,容易造成设计混乱 |
避坑指南
我曾经接手过一个项目,里面有个类声明了十几个友元类和友元函数。每次改那个类的私有成员,都要去检查所有友元有没有受影响。后来我花了整整两天重构,把大部分友元都改成了接口调用。
所以我的建议是:
- 能用公有接口解决的,就别用友元
- 友元函数比友元类更可控,优先考虑
- 友元类的数量控制在 1-2 个以内
- 如果两个类需要频繁互访私有成员,考虑是不是应该合并成一个类
友元的核心逻辑
下面这张图帮你理清友元的关系:
核心要点:友元是「被访问者主动授予」的权限,不是「访问者主动索取」的。类 A 说「B 是我的朋友」,B 才能访问 A 的私有成员。反过来不行。
实际项目中的使用建议
我个人的经验是,友元最适合用在两个场景:
- 运算符重载——特别是
<<、>>、*这些需要对称性的运算符 - 工厂模式——工厂类需要访问产品类的私有构造函数
其他情况,能不用就不用。你想想看,如果每个类都随便开友元,那 private 还有什么意义?封装性一旦被破坏,后续维护的成本会直线上升。
嗯,关于友元就聊这么多。记住一句话:友元是特例,不是常规手段。用的时候多问自己一句——真的非用不可吗?