C++运算符重载:从基础到复数类型的实现
立即解锁
发布时间: 2025-08-16 01:20:55 阅读量: 2 订阅数: 29 


C++编程语言第四版:核心概念与技术
# C++ 运算符重载:从基础到复数类型的实现
## 1. 引言
在各个技术领域,甚至多数非技术领域,都发展出了约定俗成的速记符号,以方便表达和讨论常用概念。例如,`x+y*z` 比 “multiply y by z and add the result to x” 更清晰易懂。简洁的符号对于常见操作的重要性不言而喻。
C++ 为其内置类型提供了一组运算符。然而,许多常用运算符所涉及的概念并非 C++ 的内置类型,需要用用户自定义类型来表示。比如,在 C++ 中进行复数运算、矩阵代数、逻辑信号处理或字符串操作时,我们会使用类来表示这些概念。为这些类定义运算符,有时能让程序员以更常规、便捷的符号来操作对象,而非仅依赖基本的函数表示法。
以下是一个简单的复数类示例:
```cpp
class complex {
// very simplified complex
double re, im;
public:
complex(double r, double i) :re{r}, im{i} { }
complex operator+(complex);
complex operator*(complex);
};
```
这个类定义了复数的简单实现,复数由一对双精度浮点数表示,并通过 `+` 和 `*` 运算符进行操作。程序员可以定义 `complex::operator+()` 和 `complex::operator*()` 来分别赋予 `+` 和 `*` 相应的含义。例如,如果 `b` 和 `c` 是 `complex` 类型,`b+c` 就相当于 `b.operator+(c)`。现在我们可以近似实现复数表达式的常规解释:
```cpp
void f()
{
complex a = complex{1,3.1};
complex b {1.2, 2};
complex c {b};
a = b+c;
b = b+c*a;
c = a*b+complex(1,2);
}
```
这里遵循常规的运算符优先级规则,所以第二个语句 `b = b+c*a` 实际上是 `b=b+(c*a)`,而不是 `b=(b+c)*a`。
需要注意的是,C++ 语法规定 `{}` 符号只能用于初始化和赋值的右侧:
```cpp
void g(complex a, complex b)
{
a = {1,2};
// OK: right hand side of assignment
a += {1,2};
// OK: right hand side of assignment
b = a+{1,2};
// syntax error
b = a+complex{1,2};
// OK
g(a,{1,2});
// OK: a function argument is considered an initializer
{a,b} = {b,a};
// syntax error
}
```
虽然从原理上讲,`{}` 可以在更多地方使用,但编写允许 `{}` 在表达式中随处使用的语法会带来技术难题(例如,如何判断分号后的 `{` 是表达式的开始还是代码块的开始),并且要给出良好的错误信息,因此在表达式中对 `{}` 的使用进行了更严格的限制。
运算符重载的许多明显应用场景是针对数值类型,但用户自定义运算符的用途并不局限于此。例如,在设计通用和抽象接口时,常常会使用 `->`、`[]` 和 `()` 等运算符。
## 2. 运算符函数
可以声明为以下运算符定义含义的函数:
| 运算符类型 | 运算符列表 |
| --- | --- |
| 算术运算符 | `+` `-` `*` `/` `%` `^` `&` `|` `~` `!` `=` `<` `>` `+=` `-=` `*=` `/=` `%=` `^=` `&=` `|=` `<<` `>>` `>>=` `<<=` `==` `!=` `<=` `>=` `&&` `||` `++` `--` `->*` `,` `->` `[]` `()` `new` `new[]` `delete` `delete[]` |
然而,以下运算符不能由用户定义:
- `::`:作用域解析运算符
- `.`:成员选择运算符
- `.*`:通过成员指针进行成员选择的运算符
这些运算符以名称而非值作为第二个操作数,是引用成员的主要方式。允许它们重载会带来一些微妙的问题。
另外,一些“运算符”不能重载,因为它们反映了操作数的基本事实:
- `sizeof`:获取对象的大小
- `alignof`:获取对象的对齐方式
- `typeid`:获取对象的类型信息
最后,三元条件表达式运算符 `?:` 也不能重载(没有特别根本的原因)。
此外,用户自定义字面量使用 `operator""` 符号来定义,这是一种语法技巧,因为实际上并没有名为 `"` 的运算符。类似地,`operator T()` 定义了向类型 `T` 的转换。
虽然不能定义新的运算符符号,但当现有的运算符集不够用时,可以使用函数调用表示法。例如,使用 `pow()` 而不是 `**`。这些限制看似严格,但更灵活的规则容易导致歧义。例如,定义 `**` 为指数运算符看似简单,但会引发一系列问题,如 `**` 应向左结合(如 Fortran)还是向右结合(如 Algol),表达式 `a**p` 应解释为 `a*(p)` 还是 `(a)**(p)` 等。虽然这些技术问题都有解决方案,但应用微妙的技术规则是否能使代码更易读和维护并不确定。如果有疑问,建议使用命名函数。
运算符函数的名称由关键字 `operator` 后跟运算符本身组成,例如 `operator<<`。运算符函数的声明和调用方式与其他函数相同,使用运算符只是对运算符函数显式调用的简写。例如:
```cpp
void f(complex a, complex b)
{
complex c = a + b;
// shorthand
complex d = a.operator+(b);
// explicit call
}
```
根据前面 `complex` 类的定义,这两个初始化语句是等价的。
### 2.1 二元和一元运算符
#### 2.1.1 二元运算符
二元运算符可以通过以下两种方式定义:
- 非静态成员函数,接受一个参数
- 非成员函数,接受两个参数
对于任何二元运算符 `@`,`aa@bb` 可以解释为 `aa.operator@(bb)` 或 `operator@(aa,bb)`。如果两者都有定义,则通过重载解析(§12.3)来确定使用哪种解释。例如:
```cpp
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)
}
```
#### 2.1.2 一元运算符
一元运算符(前缀或后缀)可以通过以下两种方式定义:
- 非静态成员函数,不接受参数
- 非成员函数,接受一个参数
对于任何前缀一元运算符 `@`,`@aa` 可以解释为 `aa.operator@()` 或 `operator@(aa)`。对于任何后缀一元运算符 `@`,`aa@` 可以解释为 `aa.operator@(int)` 或 `operator@(aa,int)`。如果两者都有定义,同样通过重载解析来确定使用哪种解释。
运算符只能按照语法定义的方式进行声明,例如,用户不能定义一元 `%` 或三元 `+`。以下是一些示例:
```cpp
class X {
public:
// members (with implicit this pointer):
X* operator&();
// prefix unary & (address of)
X operator&(X);
// binary & (and)
X operator++(int);
// postfix increment (see §19.2.4)
X operator&(X,X);
// error : ternary
X operator/();
// error : unary /
};
// nonmember functions :
X operator-(X);
// prefix unary minus
X operator-(X,X);
// binary minus
X operator--(X&,int);
// postfix decrement
X operator-();
// error : no operand
X operator-(X,X,X);
// error : ternary
X operator%(X);
// error : unary %
```
`[]` 运算符在 §19.2.1 中描述,`()` 运算符在 §19.2.2 中描述,`->` 运算符在 §19.2.3 中描述,`++` 和 `--` 运算符在 §19.2.4 中描述,分配和释放运算符在 §11.2.4 和 §19.2.5 中描述。
`operator=`、`operator[]`、`operator()` 和 `operator->` 必须是非静态成员函数。
默认情况下,`&&`、`||` 和逗号运算符涉及求值顺序:先计算第一个操作数,再计算第二个操作数(对于 `&&` 和 `||`,第二个操作数并非总是计算)。但对于用户自定义的 `&&`、`||` 和逗号运算符,这些特殊规则不适用,它们会像其他二元运算符一样处理。
### 2.2 运算符的预定义含义
一些内置运算符的含义被定义为其他运算符对相同操作数的组合。例如,如果 `a` 是 `int` 类型,`++a` 等价于 `a+=1`,而 `a+=1` 又等价于 `a=a+1`。但对于用户自定义运算符,除非用户明确定义,否则这些关系不成立。例如,编译器不会根据 `Z::operator+()` 和 `Z::operator=()` 的定义自动生成 `Z::operator+=()` 的定义。
对于类对象,`=`(赋值)、`&`(取地址)和 `,`(顺序)运算符有预定义的含义。这些预定义含义可以被“删除”:
```cpp
class X {
public:
// ...
void operator=(const X&) = delete;
void operator&() = delete;
void operator,(const X&) = delete;
// ...
};
void f(X a, X b)
{
a = b;
// error : no operator=()
&a;
// error : no operator&()
a,b;
// error : no operator,()
}
```
当然,也可以通过适当的定义为它们赋予新的含义。
### 2.3 运算符与用户自定义类型
运算符函数要么是类的成员,要么至少接受一个用户自定义类型的参数(重新定义 `new` 和 `delete` 运算符的函数除外)。这条规则确保用户只有在表达式包含用户自定义类型的对象时,才能改变表达式的含义。特别是,不能定义仅对指针操作的运算符函数,这保证了 C++ 的可扩展性但又不会随意改变其原有语义(类对象的 `=`、`&` 和 `,` 运算符除外)。
如果运算符函数的第一个操作数是内置类型,它不能是成员函数。例如,将整数 2 与复数变量 `aa` 相加,`aa+2` 可以通过适当声明的成员函数解释为 `aa.operator+(2)`,但 `2+aa` 无法这样解释,因为没有 `int` 类可以定义 `+` 为 `2.operator+(aa)`。即使有,也需要两个不同的成员函数来处理 `2+aa` 和 `aa+2`。由于编译器不知道用户自定义 `+` 的含义,不能假设该运算符是可交换的,因此不能将 `2+aa` 解释为 `aa+2`。这个问题可以通过一个或多个非成员函数轻松解决。
枚举类型也是用户自定义类型,因此可以为它们定义运算符。例如:
```cpp
enum Day { sun, mon, tue, wed, thu, fri, sat };
Day& operator++(Day& d)
{
return d = (sat==d) ? sun : static_cast<Day>(d+1);
}
```
每个表达式都会检查是否存在歧义。当用户自定义运算符提供了可能的解释时,会根据重载解析规则(§12.3)检查该表达式。
### 2.4 传递对象
定义运算符时,我们通常希望提供常规的符号表示,例如 `a=b+c`。因此,在向运算符函数传递参数以及函数返回值的方式上,我们的选择有限。例如,不能要求使用指针参数,然后期望程序员使用取地址运算符;也不能返回指针,然后期望用户进行解引用操作,像 `*a=&b+&c` 这样的写法是不可接受的。
对于参数,主要有两种选择:
- **值传递**:对于小对象(例如 1 到 4 个字),值传递通常是可行的选择,并且往往能提供最佳性能。不过,参数传递和使用的性能取决于机器架构、编译器接口约定(应用二进制接口,ABIs)以及参数被访问的次数(通常,访问值传递的参数比引用传递的参数更快)。例如,假设 `Point` 由一对 `int` 表示:
```cpp
void Point::operator+=(Point delta);
// pass-by-value
```
- **引用传递**:对于较大的对象,我们使用引用传递。例如,由于矩阵(一个简单的双精度矩阵)很可能比几个字大,我们使用常量引用传递:
```cpp
Matrix operator+(const Matrix&, const Matrix&);
// pass-by-const-reference
```
特别是,当传递不需要被调用函数修改的大对象时,我们使用常量引用。
通常,运算符会返回一个结果。返回
0
0
复制全文
相关推荐









