一、友元函数与运算符重载:何时需要友元
运算符重载这事儿,说白了就是让自定义类型也能用上 +、-、<< 这些运算符。但有个问题——当运算符的左侧操作数不是当前类的对象时,成员函数就玩不转了。这时候,友元函数就派上了用场。
我个人习惯把友元函数看作「被授予特权的普通函数」。它本身不是类的成员,却能访问类的私有成员。嗯,听起来有点开后门的意思,但C++就是这么设计的。
什么时候必须用友元?
我总结了一下,主要有三种场景:
- 二元运算符的左侧操作数不是当前类对象——比如
cout << obj,左侧是ostream,你没法给ostream加成员函数。 - 需要对称性运算——比如
3 + obj和obj + 3都应该能工作,但成员函数只能处理obj + 3。 - 运算符需要访问两个类的私有成员——比如矩阵乘法,需要同时访问两个矩阵的内部数据。
核心原则:如果运算符的左侧操作数不是当前类的对象,或者你需要对称访问,那就用友元。否则,优先考虑成员函数。
二、友元函数重载二元运算符的典型场景
咱们直接看代码。我拿一个 Complex 类来举例,这是最经典的场景。
场景1:输出运算符 <<
这个场景我在项目中遇到过无数次。调试的时候,能直接 cout << myObj 简直太方便了。
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 友元声明
friend ostream& operator<<(ostream& os, const Complex& c);
};
// 友元定义
ostream& operator<<(ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << "i)";
return os;
}
为什么这里必须用友元?因为 operator<< 的左侧是 ostream,不是 Complex。你想想看,你总不能去修改标准库的 ostream 类吧?
小技巧:我习惯把 operator<< 和 operator>> 都声明为友元,这样输入输出就对称了。
场景2:输入运算符 >>
跟输出类似,输入运算符也需要访问私有成员来赋值。
class Complex {
// ... 同上
friend istream& operator>>(istream& is, Complex& c);
};
istream& operator>>(istream& is, Complex& c) {
is >> c.real >> c.imag;
return is;
}
场景3:对称的算术运算符
比如 3 + obj 和 obj + 3 都应该合法。用成员函数只能实现 obj + 3,因为左侧必须是当前类对象。
class Complex {
// ...
friend Complex operator+(const Complex& lhs, const Complex& rhs);
};
Complex operator+(const Complex& lhs, const Complex& rhs) {
return Complex(lhs.real + rhs.real, lhs.imag + rhs.imag);
}
// 现在两种写法都支持:
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2; // 两个对象相加
Complex c4 = 3 + c1; // int + Complex —— 隐式转换后调用友元
这里有个细节:3 + c1 能工作,是因为 3 被隐式转换成了 Complex(3, 0)。如果构造函数是 explicit 的,那就不行了。我曾经在这个坑里摔过一次,后来就特别注意。
避坑指南:我曾经在项目里把构造函数声明为 explicit,结果 3 + obj 编译不过。排查了半天才发现是隐式转换被禁用了。解决方案是显式提供 Complex 类型的重载版本。
场景4:复合赋值运算符(+=, -= 等)
这类运算符通常用成员函数实现,因为左侧一定是当前类对象。但如果你非要对称,也可以用友元。
class Complex {
public:
// 成员函数版本 —— 推荐
Complex& operator+=(const Complex& rhs) {
real += rhs.real;
imag += rhs.imag;
return *this;
}
// 友元版本 —— 不推荐,但也可以
friend Complex& operator+=(Complex& lhs, const Complex& rhs);
};
我个人习惯用成员函数实现 +=,然后用它来实现 +。这样代码更干净。
三、友元 vs 成员函数:一张表说清楚
| 运算符类型 | 推荐方式 | 原因 |
|---|---|---|
| =, [], (), -> | 成员函数 | 语言强制要求 |
| +=, -=, *=, /= | 成员函数 | 左侧一定是当前对象 |
| +, -, *, / | 友元或成员 | 需要对称性时用友元 |
| <<, >> | 友元 | 左侧是流对象 |
| ++ (前置/后置) | 成员函数 | 修改自身状态 |
四、知识体系图
下面这张图总结了友元函数与运算符重载的核心逻辑。我画图的时候特意把「何时用友元」放在中心,因为这是最容易搞混的地方。
五、避坑指南与个人经验
最后,分享几个我踩过的坑:
- 友元不是成员函数——它没有
this指针,所以不能直接访问成员,必须通过参数传入的对象来访问。 - 友元声明的位置——放在 public、private、protected 区域效果都一样,但为了可读性,我习惯放在类的最前面或最后面。
- 不要滥用友元——友元破坏了封装性。能用成员函数解决的问题,就别用友元。我见过有人把所有运算符都写成友元,那代码简直没法维护。
- 隐式转换的陷阱——前面提过,
explicit构造函数会阻止隐式转换,导致友元重载的运算符无法处理int + obj这种写法。
我的建议:先写成员函数版本,遇到左侧不是当前类对象的情况,再改成友元。别一开始就全用友元,那样反而容易出问题。
好了,关于友元函数与运算符重载,核心就是这些。记住那个判断标准:左侧是谁?能不能改?能改就用成员,不能改就用友元。就这么简单。
公众号:蓝海资料掘金营,微信deep3321