众所周知,运算符重载作为多态的一种实现,使用是较为普遍的

但是重载运算符,本身是几元就有几个参数,对于二元运算符,第一个参数对应左侧运算对象,第二个参数对应右侧运算对象

而重载运算符作为类的成员函数的时候,第一个参数就隐式绑定了this指针,即左侧运算对象固定为this指针

举个最常见的拷贝赋值重载函数例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DeepCopy
{
public:
int *data;
//省略构造函数
DeepCopy& operator=(const DeepCopy &other)
{
if(this!=other)
{
delete data;
data=new int (*(other.data));
}
return *this;
}
};
DeepCopy a,b;
a=b;

但是,我们难免会遇到非成员函数重载的情况,此时我们就要借助友元函数或者全局函数,但友元函数相对更为常规,比如以下例子

1. 需要对称性或隐式转换支持

  • 场景:当运算符的两个操作数需要平等对待(如 +*== 等双目运算符),且可能涉及不同类型的隐式转换。
  • 原因:成员函数将左侧操作数(this)固定为类类型,限制了左侧操作数的隐式转换。而非成员函数允许左右操作数都进行隐式转换,提供更自然的语义。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass {
    int value;
    public:
    MyClass(int v) : value(v) {}
    friend MyClass operator+(const MyClass& lhs, const MyClass& rhs) {
    return MyClass(lhs.value + rhs.value);
    }
    };
    MyClass a(5), b(10);
    MyClass c = a + b; // OK
    MyClass d = 5 + a; // 非成员函数支持 int 到 MyClass 的隐式转换
    如果 operator+ 是成员函数,5 + a 会失败,因为左侧的 int 无法调用成员函数。

2. 涉及非类类型或标准库类型

  • 场景:运算符涉及自定义类与内置类型(如 intdouble)或标准库类型(如 std::string)的交互。
  • 原因:你无法修改内置类型或标准库类型的定义来添加成员函数,因此只能使用非成员函数。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MyClass {
    std::string data;
    public:
    MyClass(const std::string& d) : data(d) {}
    friend std::string operator+(const std::string& lhs, const MyClass& rhs) {
    return lhs + rhs.data;
    }
    };
    std::string s = "Hello, " + MyClass("World"); // 非成员函数支持

3. 提高封装性或避免侵入式修改

  • 场景:当你不希望修改类的定义(例如第三方库的类)或想保持类接口简洁。
  • 原因:非成员函数(通常声明为 friend 或通过公共接口访问)可以在类外部定义,减少对类内部实现的依赖。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MyClass {
    int value;
    public:
    MyClass(int v) : value(v) {}
    int getValue() const { return value; }
    };
    MyClass operator+(const MyClass& lhs, const MyClass& rhs) {
    return MyClass(lhs.getValue() + rhs.getValue());
    }
    这里无需修改 MyClass 的定义,保持了封装性。

4. 流运算符(<<>>

  • 场景:重载输入输出流运算符(如 std::ostream<<std::istream>>)。
  • 原因:这些运算符的左侧操作数是 std::ostreamstd::istream,无法修改其类定义,因此必须使用非成员函数。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass {
    int value;
    public:
    MyClass(int v) : value(v) {}
    friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
    os << obj.value;
    return os;
    }
    };
    MyClass x(5);
    std::cout << x; // 输出 5

注意事项

  • 友元 vs. 公共接口:非成员函数可以通过 friend 访问私有成员,或通过公共 getter 方法操作。前者更简洁但可能降低封装性,后者更符合封装但代码稍冗长。
  • 隐式转换的副作用:非成员函数支持双边隐式转换,可能导致意外的类型转换,需谨慎设计构造函数(考虑 explicit)。
  • 性能:非成员函数通常是内联的(inline),性能与成员函数相当,但复杂运算需注意临时对象的创建和销毁开销。

总结

建议将运算符重载定义为非成员函数的场景包括:

  1. 需要左右操作数的对称性或隐式转换(如 +*)。
  2. 涉及非类类型或标准库类型(如 std::stringstd::ostream)。
  3. 提高封装性或避免修改类定义。
  4. 流运算符(<<>>)。

关于友元的相关介绍,可以查看另一篇帖子。