引言
C++ 允许用户为自定义类型重载运算符,以实现类似于内置类型的操作。
class complex {
double re, im;
public:
complex(double r, double i) : re(r), im(i) {}
complex operator+(const complex& other);
complex operator*(const complex& other);
};
例如,如果 b
和 c
是 complex
类型,则 b + c
等价于 b.operator+(c)
。
运算符函数
我们可以声明一些新的函数,令其表示下述运算符:
然而,有些运算符是不允许用户定义的,包括:
-
::
:作用域解析 -
.
:成员选择 -
.*
:通过指向成员的指针访问成员
除了上述不可重载的运算符外,还有一些特殊的运算符和表达式不能被重载:
-
sizeof
:用于获取对象的尺寸。 -
alignof
:用于获取对象的对齐方式。 -
typeid
:用于获取对象的type_info
。 - 三元条件表达式
?:
。
C++ 不允许程序员定义全新的运算符,因为这可能会导致程序的二义性。
C++ 允许使用 operator""
来定义用户字面值常量,这是一种语法上的技巧,实际上并没有 ""
这样的运算符。
#include <iostream>
// 定义一个用户字面量 "m" 来表示米 (m)
constexpr double operator"" _m(long double x) {
return x; // 只是返回原值,通常可以做单位转换等操作
}
int main() {
double distance = 5.0_m; // 使用用户定义的字面量
std::cout << "Distance: " << distance << " meters" << std::endl;
return 0;
}
同样,operator T() 用于定义到类型 T 的类型转换。
#include <iostream>
class MyClass {
public:
operator int() const { // 将 MyClass 对象转换为 int 类型
return 42;
}
};
int main() {
MyClass obj;
int value = obj; // 隐式调用 operator int()
std::cout << "Value: " << value << std::endl;
return 0;
}
二元和一元运算符
二元运算符可以有两种定义方式(aa@bb
):
- 接受一个参数的非 static 的成员函数(
aa.operator@(bb)
) - 接受两个参数的非成员函数(
operator@(aa, bb)
)
如果两种形式都被定义了,重载解析将决定使用哪一个:
class X {
public:
void operator+(int); // 成员函数形式
X(int);
};
void operator+(X, X); // 非成员函数形式
void operator+(X, double);
void f(X a) {
a + 1; // 调用 a.operator+(1)
1 + a; // 调用 ::operator+(X(1), a)
a + 1.0; // 调用 ::operator+(a, 1.0)
}
对于一元运算符,无论是前置还是后置的,也可以有两种定义方式(@aa
、aa@
):
- 不接受任何参数的非 static 成员函数(
aa.operator@()
) - 接受一个参数的非成员函数
operator@(aa)
如果两种形式都被定义了,重载解析将决定使用哪一个。
运算符 &&
、||
和 ,
(逗号)的默认含义中包含顺序信息:它们的第一个运算对象在第二个运算对象之前求值(&&
和 ||
的第二个运算对象有可能不求值)。这一特殊的规则并不适用于用户自定义的 &&
、||
和 ,
(逗号),它们与其他二元运算符没什么不一样。
运算符的预置含义
在 C++ 中,一些内置运算符具有预置的含义,这些含义与接受相同参数的其他运算符组合的含义相同。例如,如果 a
是一个 int
类型的变量,那么 ++a
等价于 a+=1
或 a=a+1
。然而,这种预置含义并不适用于用户自定义的运算符。例如,即使类 Z
定义了 operator+()
和 operator=()
,编译器也不会自动根据这些定义生成 operator+=()
的定义。
当运算符作用于类的实例时,如赋值运算符 =
、取地址运算符 &
和逗号运算符 ,
(用于顺序执行),它们具有预置的含义。这些预置的含义可以通过使用 delete
关键字来去除,或者赋予它们新的含义。
传递对象
主要有两种参数传递方式:值传递(小对象,1-4 字节)和引用传递(大对象,如果不会被修改,可用 const
修饰)。
运算符查找机制
对于二元运算符 @
,如果 x
的类型是 X
,y
的类型是 Y
,则解析过程如下:
- 查找
X
是否有成员operator@
或者X
的基类是否有成员operator@
。 - 在
x@y
的上下文中查找是否有operator@
的声明。 - 如果
X
定义在名字空间N
中,在N
的范围内查找operator@
的声明。 - 如果
Y
定义在名字空间M
中,在M
的范围内查找operator@
的声明。
如果有多个 operator@
的声明,根据重载解析规则寻找最佳匹配。这个查找机制仅当运算符的至少一个运算对象是用户自定义类型时才会执行。
一元运算符的解析方式与二元运算符类似。
在查找运算符时,成员函数与非成员函数相比并没有明显的优势。
复数类型
本节以复数为例,介绍一些运算符重载的规则和技巧。
成员函数和非成员函数
定义运算符的小技巧:类内部定义了修改第一个参数值的运算符(如 +=
),而类外部定义了计算新值的运算符(如 +
)。这样做可以减少函数的数量,并且通过使用类内部的运算符来实现类外部的运算符。
成员运算符如 operator+=
允许直接修改对象的内容。例如:
class complex {
double re, im;
public:
complex& operator+=(const complex& a); // 需要访问类的数据成员
};
非成员运算符如 operator+
通过参数计算新值,不会修改原始对象。例如:
complex operator+(const complex& a, const complex& b) {
return a += b; // 通过+=访问类的数据成员
}
混合模式运算
为了实现混合模式运算,我们需要定义可以接受不同类型运算对象的运算符函数的重载
以复数加为例,需要定义可以接受不同类型运算对象的 operator+
。这包括三个变体:两个复数相加、复数加实数、实数加复数。这些运算符可以定义在 complex
类的外部:
complex operator+(const complex& a, const complex& b) {
return a += b; // 调用 complex::operator+=(const complex&)
}
complex operator+(const complex& a, double b) {
return a += b; // 调用 complex::operator+=(double)
}
complex operator+(double a, const complex& b) {
complex temp = b;
temp += a; // 调用 complex::operator+=(double)
return temp;
}
字面值常量
C++ 内置类型如 double
有字面值常量,例如 1.2
和 12e3
。通过将 complex
类的构造函数声明为 constexpr
,我们可以创建类似的 complex
字面值常量。constexpr
构造函数允许在编译时初始化对象。
class complex {
public:
constexpr complex(double r = 0, double i = 0) : re{ r }, im{ i } {}
private:
double re, im;
};
使用这种方式,我们可以在编译时构造 complex
字面值常量:
complex z1{1.2, 12e3}; // 使用花括号初始化
constexpr complex z2{1.2, 12e3}; // 确保在编译时初始化
如果构造函数简单且内联,尤其是 constexpr
的,我们可以很容易地将用字面值常量参数调用构造函数的结果视为字面值常量。
用户自定义字面值常量
进一步地,我们可以为 complex
类型引入用户自定义的字面值常量。例如,我们可以定义后缀 i
,它表示“虚部”。
constexpr complex operator""i(long double d) { // 虚部字面值常量
return { 0, d }; // complex是一种字面值常量类型
}
基于这个约定,我们可以写出:
complex z1{ 12e3i }; // 使用用户自定义的字面值常量
complex z2 = 12e3i; // 使用用户自定义的字面值常量
类型转换
类型转换运算符
成员函数 X::operatorT()
定义了从 x
向 T
的类型转换(如果不是 explicit
就是自动转换),其中 T
是一个类型名。
例如,定义一个只占 6 个二进制位的整数 Tiny
,并希望它能够在算术运算中与普通整数无缝融合,同时在值超出表示范围时抛出 Bad_range
异常。
class Tiny {
char v;
void assign(int i) {
if (i & ~077) throw Bad_range();
v = i;
}
public:
class Bad_range {};
Tiny(int i) { assign(i); }
Tiny& operator=(int i) { assign(i); return *this; }
operator int() const { return v; } // 转换成int的函数
};
使用类型转换运算符 operator int()
,我们可以在需要 int
的地方自动将 Tiny
转换为对应的 int
值。例如:
int main() {
Tiny c1 = 2;
Tiny c2 = 62;
Tiny c3 = c2 - c1; // c3 = 60
int i = c1 + c2; // i = 64
c1 = c1 + c2; // 越界错误: c1的值不能取64
i = c3 - 64; // i = -4
c2 = c3 - 64; // 越界错误: c2的值不能取-4
}
注意,在转换类型函数中,不要写返回值类型:
Tiny::operator int() const { return v; } // 正确
int Tiny::operator int() const { return v; } // 错误
通过在声明前加上 explicit
关键字,则必须显式调用:
class Tiny {
char v;
void assign(int i) {
if (i & ~077) throw Bad_range();
v = i;
}
public:
class Bad_range {};
Tiny(int i) { assign(i); }
Tiny& operator=(int i) { assign(i); return *this; }
explicit operator int() const { return v; }; // 转换成int的函数
};
int main() {
Tiny c1 = 2;
int i = c1; // 错误
int j = int(c1); // 正确
}