运算符重载定义和规则

当运算符用于的对象时,C++ 允许为其指定新的含义,即重载运算符。重载的运算符其实是具有特殊名字的函数,同样也包含函数名、返回类型、参数列表以及函数体。

使用运算符重载时,需要注意:

  1. 重载运算符的参数数量等于该运算符作用的运算对象数量,即一元运算符重载有一个参数,二元运算符重载有两个运算符。对于二元运算符,左侧对象传递给第一个参数,右侧对象传递给第二个参数。

  2. 若重载的运算符是类成员函数,它的左侧运算对象绑定到隐式的 this 指针上。

  3. 除重载的函数调用运算符 operator() 之外,其他重载运算符不能含有默认实参。

  4. 一个重载运算符函数必须是类的成员或者至少含有一个 class 类型的参数。所以,对于内置类型的运算对象,无法改变运算符的含义。

  5. 重载的运算符不会改变其优先级、结合律以及运算对象的个数。

  6. 只能重载已有的运算符,不能发明新的运算符;有些运算符不能被重载。

  7. 使用与内置类型一致的含义,只有当操作的含义对于用户来说清晰明了时才使用重载的运算符。

可以被重载的运算符

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

不可以被重载的运算符

运算符 名称 作用
. 成员运算符 成员选择
.* 成员指针运算符 指向成员的指针选定内容
:: 范围解析运算符 作用域范围解析
?: 条件运算符 处理简单的条件逻辑
# 预处理器符 预处理器转换为字符串
## 预处理器符 预处理器串联

范围解析运算符 ::

此处参考微软C++文档 范围解析运算符:”::” | Microsoft Docs

范围解析运算符用于标识和消除在不同作用域范围中使用的标识符,可以作用于、枚举类和命名空间

使用形式如:

1
2
3
4
5
6
:: identifier // 全局作用域
class-name :: identifier // 访问静态成员,或者定义类外函数
namespace :: identifier // 标识命名空间中的标识符
enum class :: identifier // 取得枚举类中的枚举值
enum struct :: identifier // 取得枚举结构体中的枚举值
// identifier 可以是变量、函数或者枚举值

N::m 中,无论 N 还是 m 都不是值的表达式而是编译器知道的标识符。:: 执行一个(编译期的)范围解析,而不是表达式求值。

若重载 x::yx 可能是一个对象而不是一个命名空间或者类,这样就会导致产生与原来的表现相反的新语法:expr1 :: expr2。还需要对表达式求值,显然会增加复杂性。

条件运算符 ?:

?: 也叫三目运算符,使用形式如 cond ? expr1 : expr2; 。执行过程如下:

  • cond 的值;
  • 若条件为真,求 expr1的值并返回该值;
  • 若条件为假,求 expr2的值并返回该值。

所以,条件运算符保证两个表达式中只有一个会被执行,也具有短路特性。重载可能会失去这个特性。

此外,几乎也没有场景需要重载条件运算符。

长度运算符 sizeof

sizeof 是一个编译时运算符,用于判断变量或数据类型的字节大小;也可用于获取类、结构、共用体和其他用户自定义数据类型的大小。返回值是一个 size_t 类型。 它有两种使用形式:

1
2
sizeof (type); // 计算类型所占字节数
sizeof expr; // 计算表达式结果类型所占字节数,但不会对实际运算对象(即表达式)求值

sizeof 不能被重载是因为有一些内建操作(built-in operations)会依靠它的结果,比如对一个指向数组的指针进行自增:

1
2
3
4
5
6
7
8
9
10
11
T a[3];
T *p = &a[0];
T *q = &a[0];
p ++;
long long llp = reinterpret_cast<long long>(p);
long long llq = reinterpret_cast<long long>(q);
cout << llp << endl;
cout << llq << endl;
cout << llp-llq << endl;
cout << sizeof(T) << endl;
// llp - llq == sizeof(T)

sizeof 被重载,由程序员赋予了一个新的含义,可能会违反基本的语法,产生一些意想不到的结果。

成员运算符 . .*

点运算符.和 箭头运算符->用于引用类 class、结构体struct和共用体union的成员。点运算符应用于实际的对象。

理论上来说,.(点运算符)可以通过使用和 -> 一样的技术来进行重载。但是,这样做会导致一个问题,那就是无法确定操作的是重载了 . 的对象呢,还是通过 . 引用的一个对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Y {
public:
void f();
// ...
};
class X { // 假设你能重载.
Y* p;
Y& operator.() { return *p; }
void f();
// ...
};
void g(X& x){
x.f(); // X::f 还是Y::f 还是错误?
}

为了避免产生歧义,重载点运算符是不被允许的。

不建议重载的运算符

逻辑与运算符 && 和 逻辑或运算符 ||

逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值;并且只有当左侧运算对象无法确定整个表达式的结果时才会计算右侧运算对象的值,这种策略称为短路求值(short-circuit evaluation)。

重载后无法保留上述求值顺序和短路求值特性

取地址运算符 & 和 逗号运算符 ,

首先要明白一点:运算符重载是与类高度绑定的,而C++语言已经定义了这两种语言用于 class 类型时的特殊含义,即已经有内置含义,如果被重载,它们的行为将异于常态,导致类的用户无法适应。

类中的逗号运算符一般是在初始化列表中出现。

而取地址运算符是编译器给类自动生成的六个默认成员函数之一,返回对象的地址。至于对象的地址究竟是什么,这是另外一件值得研究的事了。。

总结

一般来说,慎重使用运算符重载;要使用运算符重载时,必须遵循上述规则,且要注意不能和不建议重载的运算符。

本篇参考《C++ Primer》、cpp referrence以及其他资料,个人理解,一家之言,若有改正会持续更新。下一篇讨论常见运算符的重载实现。