Skip to content

Chapter 8 Overloaded Operators

1 Basic Ideas

运算符重载的目的是让用户自定义类型像内置类型一样使用,本质上,运算符重载只换了一种语法形式的函数调用。

可重载的运算符

+ - * / % ^ & | ~
= < > += -= *= /= %=
^= &= |= << >> >>= <<= ==
!= <= >= ! && || ++ --
, ->* -> () []
operator new / delete
operator new[] / delete[]

不能重载的运算符

.   .*   ::   ?:
sizeof   typeid
static_cast dynamic_cast const_cast reinterpret_cast
Pointer to members
#include <iostream>
using namespace std;

class MyClass {
public:
    int data;
    void display() {
        cout << data << endl;
    }
};

int main() {
    MyClass obj;
    obj.data = 42;

    // 指向成员变量的指针
    int MyClass::*ptrToData = &MyClass::data;

    // 指向成员函数的指针
    void (MyClass::*ptrToFunc)() = &MyClass::display;

    // 通过对象实例和成员变量指针访问成员变量
    cout << obj.*ptrToData << endl;    // 输出 42

    // 通过对象实例和成员函数指针调用成员函数
    (obj.*ptrToFunc)();                // 等价于 obj.display(),输出 42

    return 0;
}

只能重载已经存在的运算符,不能改变运算符的操作数个数和运算符优先级。

2 How to overload

2. 1 Member Functions

隐含第一个参数(即接收者 this),接收者不做类型转换。

示例
class Integer
{
public:
    // 参数 n 默认值为 0,初始化列表将成员变量 i 初始化为 n
    Integer( int n = 0 ) : i(n) {}
    Integer operator+(const Integer& n) const {
        // 实现加法逻辑:当前对象的 i + 传入对象的 n.i
        // 返回一个新的 Integer 对象,保存相加的结果
        return Integer(i + n.i);
    }

private:
    int i;
};

Integer x(1), y(5), z;
x + y; // 等价于 x.operator+(y)

z = x + y;  // 合法,解析为 x.operator+(y)
z = x + 3;  // 合法,3 会被隐式转换为 Integer(3),解析为 x.operator+(Integer(3))
z = 3 + y;  // 非法,3 不是 Integer 对象,无法调用成员函数 operator+

成员函数特点能直接访问私有成员,要求调用者对象已经是该类类型。

  • 对于二元运算符(如 +-* 等),成员函数形式的重载只需要 1 个参数,左操作数由隐式的 this 指针传入
  • 对于一元运算符(如一元负号 -、逻辑非 ! 等),成员函数形式的重载不需要显式参数
示例

```cpp Integer operator-() const { return Integer(-i); }

z = -x; // 解析为 z.operator=(x.operator-())

2. 2 Global Functions

两个参数都显式写出且都可以执行类型转换,若要访问私有成员,可以声明为友元函数。

示例
class Integer {
public:
    friend Integer operator+(const Integer&, const Integer&);
private:
    int i;
};

Integer operator+(const Integer& lhs, const Integer& rhs) {
    return Integer(lhs.i + rhs.i);
}

z = x + y; // operator+(x, y)
z = x + 3; // operator+(x, Integer(3))
z = 3 + y; // operator+(Integer(3), y)
z = 3 + 7; // 普通 int 相加(不会触发重载)

2. 3 Comparison

  • 一元运算符优先写成成员函数
  • =, (), [], ->, ->* 必须是成员函数
  • 其他二元运算符通常更适合写成非成员函数
  • 重载应尽量保持与内置类型一致的直觉语义
  • 只读对象优先 const& 传参(内置类型除外)
  • 不修改对象状态的成员运算符要加 const (如布尔运算符、+- 等)
  • 对于全局函数,如果左侧操作数会被修改,要以引用的方式传入(比如流插入运算符<<
  • operator+ 这类应返回新对象,比较运算符返回 bool

原型习惯

// 算术运算
T operatorX(const T& l, const T& r);

// 比较运算
bool operatorX(const T& l, const T& r);

// 下标
E& T::operator[](int index);

3 Prefix and Postfix ++ / --

How to distinguish postfix from prefix?

后置形式会接收一个 int 类型参数,编译器会自动传入 0 作为这个参数的值。

示例
class Integer {
public:
    Integer& operator++();   // prefix++
    Integer operator++(int); // postfix++
    Integer& operator--();   // prefix--
    Integer operator--(int); // postfix--
};

// 前置自增
Integer& Integer::operator++() {
    *this += 1;
    return *this;
}

// 后置自增
Integer Integer::operator++(int) {
    Integer old(*this);
    ++(*this);
    return old;
}

Integer x(5);
++x; // x.operator++()
x++; // x.operator++(0)
--x; // x.operator--()
x--; // x.operator--(0)

用户自定义类型中,前置通常比后置更高效,因为后置通常需要保留旧值副本。

Relational Operators

!= 可以基于 ==>, >=, <= 可以基于 <

示例
bool Integer::operator==(const Integer& rhs) const {
    return i == rhs.i;
}

bool Integer::operator!=(const Integer& rhs) const {
    return !(*this == rhs);
}

bool Integer::operator<(const Integer& rhs) const {
    return i < rhs.i;
}

bool Integer::operator>(const Integer& rhs) const {
    return rhs < *this;
}

bool Integer::operator<=(const Integer& rhs) const {
    return !(rhs < *this);
}

bool Integer::operator>=(const Integer& rhs) const {
    return !(*this < rhs);
}

4 operator[] and operator=

operator[] 必须是成员函数,一般应返回引用,表示对象像数组一样可被下标访问。

示例
Vector v(100);  // 创建一个大小为 100 的 Vector 对象
v[10] = 45;     // 给下标为 10 的元素赋值

如果 operator[] 返回的是指针,那么赋值时就必须写成 *v[10] = 45;,会失去数组的直观写法。

Copying vs. Initialization

MyType b;
MyType a = b; // 初始化:走拷贝构造
a = b;        // 赋值:走 operator=

赋值运算符 operator= 必须是成员函数,应返回 *this 的引用,以支持链式赋值 A = B = C(实际执行顺序为:A = (B = C))。

  • 对于包含动态分配内存的类,必须显式声明赋值运算符,同时也要声明拷贝构造函数
  • 若未自定义,编译器会生成默认的逐成员赋值(memberwise assignment)
  • 如果要禁止赋值操作,可以将 operator= 显式声明为私有成员,或使用 =delete;(C++11 及以上)
  • 需要确保为所有数据成员赋值,尤其是指针类型成员,避免内存泄漏
  • 必须检查自赋值(self-assignment)情况,如 a = a;
示例
T& T::operator=(const T& rhs) {
    // check for self assignment
    if (this != &rhs) {
        // perform assignment
    }
    return *this;
}

//This checks address, not value (*this != rhs)

仿函数(functor)

重载了函数调用运算符的类 / 结构体对象,它可以像普通函数一样被调用,常用于 STL 算法中的谓词、比较器、回调对象。

struct F {
    void operator()(int x) const {
        std::cout << x << "\n";
    }
}; // F is a functor

F f;
f(2);      // calls f.operator()

operator[] 常见成对重载

class Vector {
public:
    int& operator[](int index) { return data[index]; }
    const int& operator[](int index) const { return data[index]; }
private:
    int data[100];
};

5 User-defined Type Conversions

用户可以通过类型转换运算符,将一个类的对象转换为另一个类的对象或内置类型(如 int、double 等)。

5. 1 Single-argument Constructors

示例

class PathName {
    string name;
public:
    // or could be multi-argument with defaults
    PathName(const string&);
    ~ PathName();
};

string abc("abc");
PathName xyz(abc); // OK
xyz = abc;         // 隐式转换后也 OK

explicit 可用于阻止不必要的隐式转换。

示例

class PathName {
    string name;
public:
    explicit PathName(const string&);
};

PathName xyz(abc); // OK
// xyz = abc;      // error

5. 2 Conversion Operator

函数会被编译器自动调用(隐式转换),返回类型与函数名相同。

示例
class Rational {
public:
    operator double() const {
        return numerator_ / (double)denominator_;
    }
};

Rational r(1, 3);
double d = 1.3 * r; // r => double

一般形式为 X::operator T(),其中 T 可以是内置类型或自定义类类型,没有显式参数和返回类型写法,表示把 X 转换为 T

5. 3 Type Conversion Notes

内置转换
  • 基本数值类型之间可做标准转换,如 charshortintfloatdoubleintlong
  • 数组、指针、引用、const 之间也有一套内建转换规则
    • TT&(对象到引用)
    • T&T(引用到对象)
    • T*void*(任意指针到无类型指针)
    • T[]T*(数组到指针)
    • T*T[](指针到数组)
    • Tconst T(对象到常对象)

对于用户自定义类型转换(T → C,若 C(T) 合法,则可从 T 转为 C,若 T 定义了 operator C(),也可转为 C

用户自定义转换很容易引入二义性和意外匹配,通常更推荐显式函数,如 double to_double() const;,若构造函数只用于显式创建对象,应优先考虑 explicit

最佳匹配(best match)

  • 编译器会为每个参数寻找代价最小的匹配
  • 一般优先级:精确匹配、内置类型转换、用户自定义转换

不是所有运算符都值得重载,只有当重载确实能提升代码可读性和可维护性时才去做,一旦同一个表达式存在多条可行转换路径,就可能出现重载歧义。