总目录 > 1 语言基础 > 1.5 C++ 进阶 > 1.5.3 运算符重载
前言
运算符重载是个强大而有意思的功能,最初的接触是因为不清楚如何使用 sort 从大到小排序而了解到了运算符重载,然后是得知如何实现 struct 的 sort 排序,很长一段时间没有在别的情况下再使用,可以说仅仅是冰山一角了。
子目录列表
1、概念
2、二元运算符重载
3、一元运算符重载
4、特殊运算符重载
1.5.3 运算符重载
1、概念
在 1.1 C 语言基础 中,我们已经大致介绍了运算符的概念。运算符丰富而强大,每个运算符能够操作多种数据类型,比如简单的 “+” 号能够在 int, double, char, ... 等等数据类型中使用。其实在 C++ 中,运算符并不被看作一个简单的符号,它其实属于一种特殊的函数,称为运算符函数,和其他函数一样,运算符函数也可以被重载,称为运算符重载。但是,运算符重载的命名规则和参数确定却有不同,其一般形式为:
数据类型 operator 重载运算符(重载参数表) { ... }
其中,operator 是特别标示运算符函数的限定词。
比如上述的加法,其实在 C++ 中内置了形式如下的加法运算符的重载函数:
int operator + (int, int); double operator + (double, double); char operator + (char, char);
有了运算符重载函数,运算符的适用范围就不仅局限于基本数据类型了,举个例子:
class Student { int id; double grade; public: ... }; Student A, B, C, D; cout << A.grade + B.grade + C.grade + D.grade;
假设对于包含 id 和 grade 的 Student 类,我们希望在计算各名学生的成绩 grade 时,可以直接相加而不是每次都需要调用该成员,那么我们添加一个运算符重载函数,告知编译器:我们直接相加 Student 类的意义是直接相加他们的 grade 值,如下:
Student operator + (Student a, Student b) { return a.grade + b.grade; } cout << A + B + C + D;
这是一个二元运算符的例子,运算符重载可以分为二元、一元运算符及一些特殊运算符重载,下面将一一进行介绍。
重载运算符也有许多限制。首先,并不是所有运算符都可以被重载,尽管大多数确实可以。下列运算符的重载功能受到了限制:
① 不能被重载
. .* :: ?:
② 只能被重载为类成员函数
= [] () ->
运算符重载过程中,原有含义、优先级、结合顺序和所需要的参数个数均不会被改变,更不能创造新的运算符。
2、二元运算符重载
二元运算符就是需要两个操作数的运算符,又称为双目运算符,比如 +, -, *, /, % 等等。对于二元运算 a @ b(@ 表示任意可重载二元运算符),可能会被解析为:
> a.operator@(b)
> operator@(a, b)
前者是被重载为类的费静态成员函数,要求第一个参数必须为一个对象;后者是被解析为类的友元或普通重载函数。
① 非静态成员函数重载运算符
以这种方式重载二元运算符时,只能够有一个参数,因为它实际上是函数的第二个参数,第一个参数由 this 指针隐式传递。一般形式如下:
class 类名 { ... 数据类型1 operator 重载运算符 ([类名 *this,] 数据类型2 参数名) { ... } };
数据类型 1 和 2 同样与类名是一致的。
② 友元 / 普通函数重载运算符
以这种方式重载时,需要两个参数。一般形式如下:
class 类名 { ... [friend] 数据类型1 operator 重载运算符 (数据类型2 参数名, 数据类型3 参数名) { ... } };
同样地,数据类型 1, 2, 3 通常与类名一致。
下面的代码囊括了上述两种重载方式:
class Complex { double r, i; public: ... Complex operator + (Complex b) { return Complex(r + b.r, i + b.i); } friend Complex operator - (Complex a, Complex b) { return Complex(a.r - b.r, a.i - b.i); } };
一般情况下两种方式均可使用,但对于不要求返回左值且可以交换参数次序的运算符函数,最好使用第 ② 种,因为参数与运算符所需类型不匹配时,编译器会隐式转换,而对于第 ① 种,隐式传递的指针并不会被转换类型。
3、一元运算符重载
一元运算符,顾名思义,只需要一个运算参数,比如 ++, -- 等。对于一元运算 @a(前缀一元运算)或者 a@(后缀一元运算),可能被解析为:
> a.operator@()
> operator@(a)
同上,两者分别为非静态成员函数重载和友元 / 普通函数重载。对于二元运算符,其分别需要一个和两个参数,同理,对于一元运算符,分别需要零个和一个参数。下面的代码囊括了这两种重载方式:
class Time { int h, m, s; public: ... Time& operator ++ () { ... return *this; } friend Time& operator -- (Time &t) { ... return t; } };
我们注意到,不同于二元运算符的代码,这里的函数返回类型均加上了 ‘&’,表示返回对象的引用,如果删除,则不能实现连续的运算。
上述对自增自减的重载均为前缀一元运算,因为这些运算符既可以作为前缀也可以作为后缀,而为了进行区分,需要重载后缀一元运算时,则在参数表中附加一个无用的形式参数,比如:
Time& operator ++ (int) { h++; return *this; } friend Time operator -- (Time& t, int) { t.h--; return t; }
4、特殊运算符重载
① 数组下标运算符
数组下标本质上是一种运算,用运算符 [ ] 表示,是一种二元运算符,同样允许被重载,通过重载可以检查数组的大小,并可在访问数组元素时检查下标值是否越界,这些都是编译器原生没有支持的。
[ ] 可以同时出现在赋值符的左侧和右侧,所以重载时常返回引用;只能被重载为类的非静态成员函数。
举个例子:
class Person { char name; double salary; public: ... double& operator [] (char *n) { return this -> salary; } };
(好像有点问题,或者是没体现出啥)
② 赋值运算符
对于赋值运算符 =,在 1.5.1 类与对象 中的构造函数部分进行了介绍,此处省略。
③ 类型转换运算符
类型转换在 1.2 C 语言数据类型 中有所介绍,其实类的构造函数具有类型转换的功能。再来回顾一下构造函数:
class Date { ... public: Date(int y, int m = 1, int d = 1) { ... } }; Date d(2020, 10, 3); d = 2019;
执行 d = 2019 语句时将实现类型转换,int 类型的 2019 将被隐式转换为 Date(2019)。
构造函数只能实现其他类型向类类型的转换,反之,则需要手动重载,暂略。
④ 函数调用运算符
暂略。
⑤ I / O 运算符
默认情况下,C++ 的输入输出运算符 << 和 >> 只支持基本数据类型,如果要实现对自定义数据类型的输入输出,则需要进行重载。
以输出运算符 << 为例,一般形式如下:
ostream& operator << (ostream& 输出流对象名, 类名 对象名) { ... 输出流对象名 << ...; return 输出流对象名; }
ostream 是定义与 <iostream> 头文件的输出流类,是一个内置类。<< 是一个二元运算符,第一个参数是 ostream 类的对象的引用,第二个参数是要输出的对象,由于这个过程不会改变输出对象的值,所以通常将其设置为 const 类型的引用,而无需复制实参以提高效率。
输出运算符通常被重载为类的友元函数而非成员函数,因为被重载为成员函数时第一个参数必须是通过 this 指针传递的当前对象,而不是 ostream 类对象的引用。
输入运算符 >> 基本同理,略。