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¶
内置转换
- 基本数值类型之间可做标准转换,如
char→short→int→float→double和int→long - 数组、指针、引用、
const之间也有一套内建转换规则T→T&(对象到引用)T&→T(引用到对象)T*→void*(任意指针到无类型指针)T[]→T*(数组到指针)T*→T[](指针到数组)T→const T(对象到常对象)
对于用户自定义类型转换(T → C),若 C(T) 合法,则可从 T 转为 C,若 T 定义了 operator C(),也可转为 C。
用户自定义转换很容易引入二义性和意外匹配,通常更推荐显式函数,如 double to_double() const;,若构造函数只用于显式创建对象,应优先考虑 explicit。
最佳匹配(best match)
- 编译器会为每个参数寻找代价最小的匹配
- 一般优先级:精确匹配、内置类型转换、用户自定义转换
不是所有运算符都值得重载,只有当重载确实能提升代码可读性和可维护性时才去做,一旦同一个表达式存在多条可行转换路径,就可能出现重载歧义。