C++类基础11——运算符重载

基本概念

重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。

重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。

一元运算符有一个参数,二元运算符有两个。

对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。

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

如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。

当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量少一个。

对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:

//错误:不能为int重定义内置的运算符
int operator+(int, int);

这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。

我们可以重载大多数(但不是全部)运算符。

可以被重载的运算符
+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [ ] ()
-> ->* new new[] delete delete[ ]
不能被重载的运算符
:: .* . ?:

我们只能重载已有的运算符,而无权发明新的运算符号。

例如,我们不能提供operator**来执行幂操作。

有四个符号(+、-、*、&)既是一元运算符也是二元运算符,所有这些运算符都能被重载,从参数的数量我们可以推断到底定义的是哪种运算符。

对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。不考虑运算对象类型的话,x=y+ z;永远等价于x==(y+z)。

直接调用一个重载的运算符函数

通常情况下,我们将运算符作用于类型正确的实参,从而以这种间接方式“调用”重载的运算符函数。

然而,我们也能像调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参:

// 一个非成员运算符函数的等价调用
datal + data2; // 普通的表达式
operator+(datal, data2); //等价的函数调用

这两次调用是等价的,它们都调用了非成员函数operatort,传入datal作为第一个实参、传入data2作为第二个实参。

我们像调用其他成员函数一样显式地调用成员运算符函数。

具体做法是,首先指定运行函数的对象(或指针)的名字,然后使用点运算符(或箭头运算符)访问希望调用的函数:

datal += data2;// 基于“调用”的表达式
datal.operator+=(data2); //对成员运算符函数的等价调用

这两条语句都调用了成员函数operator+=,将this绑定到datal的地址、将data2作为实参传入了函数。

某些运算符不应该被重载

某些运算符指定了运算对象求值的顺序。

因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。

特别是,逻辑与运算符、逻辑或运算符和逗号运算符的运算对象求值顺序规则无法保留下来。

除此之外,&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。

因为上述运算符的重载版本无法保留求值顺序和/或短路求值属性,因此不建议重载它们。

当代码使用了这些运算符的重载版本时,用户可能会突然发现他们一直习惯的求值规则不再适用了。

还有一个原因使得我们一般不重载逗号运算符和取地址运算符;

C++语言已经定义了这两种运算符用于类类型对象时的特殊含义,这一点与大多数运算符都不相同。因为这两种运算符已经有了内置的含义,所以一般来说它们不应该被重载,否则它们的行为将异于常态,从而导致类的用户无法适应。

 通常情况下,不应该重载逗号、取地址符、逻辑与和逻辑或运算符。

使用与内置类型一致的含义

当你开始设计一个类时,首先应该考虑的是这个类将提供哪些操作。

在确定类需要哪些操作之后,才能思考到底应该把每个类操作设成普通函数还是重载的运算符。

  • 如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符:
  • 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致。
  • 如果类的某个操作是检查相等性,则定义operator==;如果类有了operator==,意味着它通常也应该有operator!=。
  • 如果类包含一个内在的单序比较操作,则定义operator<;如果类有了operator<,则它也应该含有其他关系操作。
  • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回bool,算术运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符则应该返回左侧运算对象的一个引用。

提示:尽量明智地使用运算符重载
每个运算符在用于内置类型时都有比较明确的含义。以二元+运算符为例,它明显执行的是加法操作。因此,把二元+运算符映射到类类型的一个类似操作上可以极大地简化记忆。例如对于标准库类型string来说,我们就会使用+把一个string对象连接到另一个后面,很多编程语言都有类似的用法。

当在内置的运算符和我们自己的操作之间存在逻辑映射关系时,运算符重载的效果最好。此时,使用重载的运算符显然比另起一个名字更自然也更直观。不过,过分滥用运算符重载也会使我们的类变得难以理解。

在实际编程过程中,一般没有特别明显的滥用运算符重载的情况。例如,一般来说没有哪个程序员会定义operator+并让它执行减法操作。然而经常发生的一种情况是,程序员可能会强行扭曲了运算符的“常规”含义使得其适应某种给定的类型,这显然是我们不希望发生的。

因此我们的建议是:只有当操作的含义对于用户来说清晰明了时才使用运算符。如果用户对运算符可能有几种不同的理解,则使用这样的运算符将产生二义性。

赋值和复合赋值运算符

赋值运算符的行为与复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。

重载的赋值运算应该继承而非违背其内置版本的含义。

如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符。

无须赘言,+=运算符的行为显然应该与其内置版本一致,即先执行+,再执行=。

选择作为成员或者非成员

当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一
个普通的非成员函数。在某些时候我们别无选择,因为有的运算符必须作为成员;另一些情况下,运算符作为普通函数比作为成员更好。

下面的准则有助于我们在将运算符定义为成员函数还是普通的非成员函数做出抉择:

  • 赋值(=)、下标([ ])、调用(( ))和成员访问箭头(->)运算符必须是成员。
  • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。

程序员希望能在含有混合类型的表达式中使用对称性运算符。

例如,我们能求一个int和一个double的和,因为它们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法是对称的。如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。

当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。例如:

string s = "world";
string t=s + "!";//正确:我们能把一个const char*加到一个string对象中

string u="hi” + s; //如果+是string的成员,则产生错误

如果operator+是string类的成员,则上面的第一个加法等价于s.operator+(”!”)。

同样的,"hi"+s等价于"hi".operator+(s)。显然"hi"的类型是const char*,这是一种内置类型,根本就没有成员函数。

因为 string 将+定义成了普通的非成员函数,所以"hi"+s等价于operator+("hi",s)。

和任何其他函数调用一样,每个实参都能被转换成形参类型。

唯一的要求是至少有一个运算对象是类类型,并且两个运算对象都能准确无误地转换成string。

举例

重载输出运算符

通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用。

之所以ostream 是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。

第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。第二个形参是引用的原因是我们希望避免复制实参;而之所以该形参可以是常量是因为(通常情况下)打印对象不会改变对象的内容。

为了与其他输出运算符保持一致,operator<<一般要返回它的 ostream形参。

举个例子

#include <iostream>

class Point {
public:
    int x, y;

    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // 重载 << 运算符,用于输出
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
};

// 重载 << 运算符的定义
std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")"; // 输出点的坐标
    return os; // 返回输出流对象,以便可以链式调用
}

int main() {
    Point p(1, 2);
    std::cout << p << std::endl; // 输出: (1, 2)
    return 0;
}

输出运算符尽量减少格式化操作

用于内置类型的输出运算符不太考虑格式化操作,尤其不会打印换行符,用户希望类的输出运算符也像如此行事。

如果运算符打印了换行符,则用户就无法在对象的同一行内接着打印一些描述性的文本了。

相反,令输出运算符尽量减少格式化操作可以使用户有权控制输出的细节。

通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

输入输出运算符必须是非成员函数

与iostream 标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数

否则,它们的左侧运算对象将是我们的类的一个对象:

Point<<cout;


假设输入输出运算符是某个类的成员,则它们也必须是 istream或ostream的成员。然而,这两个类属于标准库,并且我们无法给标准库中的类添加任何成员。

所以IO运算符一般被声明为友元函数

重载输入运算符>>

通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。

该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中。

#include <iostream>

class Point {
public:
    int x, y;

    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // 重载 >> 运算符,用于输入
    friend std::istream& operator>>(std::istream& is, Point& p);
};

// 重载 >> 运算符的定义
std::istream& operator>>(std::istream& is, Point& p) {
    is >> p.x >> p.y; // 从输入流中读取x和y的值
    return is; // 返回输入流对象,以便可以链式调用
}

int main() {
    Point p;
    std::cout << "Enter x and y: ";
    std::cin >> p; // 从标准输入读取x和y的值
    std::cout << "You entered: " << p << std::endl; // 输出点的坐标
    return 0;
}


输入运算符必须处理输入可能失败的情况,而输出运算符不需要。

输入时的错误

在执行输入运算符时可能发生下列错误:

  • 当流含有错误类型的数据时读取操作可能失败。例如在读取完p.x后,输入运算符假定接下来读入的是两个数字数据,一旦输入的不是数字数据,则读取操作及后续对流的其他使用都将失败。
  • 当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败。

当读取操作发生错误时,输入运算符应该负责从错误中恢复。

标示错误

一些输入运算符需要做更多数据验证的工作。

例如,我们的输入运算符可能需要检查p.x是否符合规范的格式。在这样的例子中,即使从技术上来看IO是成功的,输入运算符也应该设置流的条件状态以标示出失败信息。

通常情况下,输入运算符只设置failbit。除此之外,设置eofbit表示文件耗尽,而设置badbit表示流被破坏。最好的方式是由IO标准库自己来标示这些错误。

示例

下面是一个改进的示例

我们可以在重载的>>运算符中添加一些错误检查代码,以确保从输入流中读取数据时能够正确处理可能的错误情况。

#include <iostream>
#include <limits>

class Point {
public:
    int x, y;

    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // 重载 >> 运算符,用于输入,并添加错误检查
    friend std::istream& operator>>(std::istream& is, Point& p);
};

// 重载 >> 运算符的定义,并添加错误检查
std::istream& operator>>(std::istream& is, Point& p) {
    // 清除任何可能的错误状态
    is.clear();

    // 尝试读取x和y的值
    if (!(is >> p.x >> p.y)) {
        // 如果读取失败,设置错误状态并输出错误信息
        is.setstate(std::ios::failbit);
        std::cerr << "Error reading Point coordinates." << std::endl;
    }

    // 返回输入流对象,以便可以链式调用
    return is;
}

int main() {
    Point p;
    std::cout << "Enter x and y: ";
    
    // 尝试从标准输入读取x和y的值
    if (!(std::cin >> p)) {
        // 如果读取失败,输出错误信息并退出程序
        std::cerr << "Failed to read Point." << std::endl;
        return 1; // 返回非零值表示程序异常退出
    }

    std::cout << "You entered: " << p << std::endl; // 输出点的坐标
    return 0; // 返回零值表示程序正常退出
}


在这个改进后的示例中,我们在operator>>中添加了错误检查代码。首先,我们使用is.clear()来清除任何之前的错误状态,确保我们的错误检查是针对当前读取操作的。然后,我们尝试从输入流is中读取p.x和p.y的值。如果读取失败(例如,用户输入了非数字字符),is >> p.x >> p.y的表达式将返回false。

在读取失败的情况下,我们使用is.setstate(std::ios::failbit)来设置输入流的错误状态,并通过std::cerr输出错误信息。这样,调用者可以通过检查输入流的状态来确定是否读取成功。

在main函数中,我们使用条件语句来检查std::cin >> p的返回值。如果读取失败,我们输出错误信息并返回非零值,表示程序异常退出。如果读取成功,我们则继续执行后续的代码。

通过这种方式,我们可以确保在读取数据时能够及时发现并处理可能的错误情况。

最近更新

  1. TCP协议是安全的吗?

    2024-04-06 16:48:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-06 16:48:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-06 16:48:03       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-06 16:48:03       20 阅读

热门阅读

  1. tomcat处理Http请求流程的步骤

    2024-04-06 16:48:03       15 阅读
  2. Promise-以往的异步编程模式

    2024-04-06 16:48:03       17 阅读
  3. Acwing.504 转圈游戏(带取余的快速幂)

    2024-04-06 16:48:03       14 阅读
  4. 【一】Mac 本地部署大模型

    2024-04-06 16:48:03       17 阅读
  5. 使用Python的SQLite和Tkinter库来创建一个简单的查询

    2024-04-06 16:48:03       20 阅读
  6. Qt 线程

    2024-04-06 16:48:03       13 阅读
  7. Python数据分析十七

    2024-04-06 16:48:03       17 阅读
  8. 基于C语言实现 SQL数据库和链表的相互转换

    2024-04-06 16:48:03       13 阅读
  9. 使用 PyArmor 加密一个 Python 包

    2024-04-06 16:48:03       17 阅读
  10. Ubuntu系统下安装SQLite Browser教程

    2024-04-06 16:48:03       15 阅读