运算符重载(Operator overloading)

从我们在几个前篇的类的层次介绍中可以知道,C++可以扩展标准运算符,使其适用于新类型。这种技术称为运算符重载。 例如,字符串类重载+运算符,使其在应用于字符串时的行为会有所不同。 当C++编译器看到 + 运算符时,它通过查看操作数的类型来决定使用的是哪种运算,如果编译器看到 + 应用于两个整数,它会采用我们一般的数字相加。 如果操作数是字符串,那么实现的就是两个字符串之间的连接。
重载运算符的能力是C++的一个强大功能,可以使程序更容易阅读,但只有当每个运算符在其应用类型之间保持一致时才能应用。例如,重载+运算符的类用于概念上类似于加法的操作,例如连接字符串。如果你写一个表达式:

s1 + s2;
1
1
对于类型字符串的两个变量,很容易将此操作视为将字符串添加到一起的操作。但是,如果重新定义一个操作符,那么会使得读者不知道它是什么意思,这时操作符重载会使程序基本上无法读取。因此,我们应该把这种技术限制在只有在提高程序可读性的情况下才能使用此功能。

重载插入运算符(Overloading the insertion operator)

从之前的中的point.h接口可以看出,Point类导出一个名为toString的方法,它将Point对象转换为包含括号中的坐标值的字符串。包括这种方法的主要目的是使得容易显示一个Point的值。调试程序时,显示变量的值通常很方便。 要显示名为pt的Point变量的值,我们需要做的就是将一个语句添加到程序中,如下所示:

cout << "pt = " << pt.toString() << endl;
1
1
运算符重载使得进一步简化此过程成为可能。 C++已经重载流插入运算符<<,以便它可以显示字符串以及原始类型。但是如果我们括重载此运算符以支持Point类,那么可以简化前面的语句为:

cout << "pt = " << pt << endl;
1
1
毫无疑问,这里有一些小小的变化,但是对比上面的那句话来说,我们却是不用为输出思考那么多。
C++中的每个运算符与用于定义其重载行为的函数名相关联。函数名称由关键字 operator 组成,后跟运算符符号。 例如,如果要为某些新类型重新定义 + 运算符,则可以定义一个名为operator +的函数,该函数接受该类型的参数.同样,可以通过为函数运算符<<提供新的定义来重载插入运算符。

编写运算符 << 函数的最难的部分是编写它的原型。<< 操作符的左操作数是输出流。对于这个参数,选择实现插入运算符的最通用类——ostream。 << 的右侧操作数是要插入该流的值,在此示例中为Point对象。运算符<<的重载定义因此有两个参数:一个ostream和一个Point。

ostream & operator<=(ostream & os, Point pt);
1
1
通过引用返回结果

但是,完成原型需要谨慎考虑流不能值传递的事实,不然函数就没了意义。这个限制意味着ostream参数必须通过引用传递。 但是,当 << 运算符返回时也会出现同样的限制。插入运算符通过返回输出流实现其美妙的连接行为(就是把输出的东西连在一起输出,例如 cout << “hello” <<”world”,虽然输出的都是一个单词,但是两个 << 运算符就是把这个值存入流,然后将其转发到链中的下一个<<运算符。从而完成链接) 为了避免在流程的这一端复制流,运算符<<的定义也必须通过引用返回其结果。
通过引用返回结果比通过引用传递参数更少见。 也就有几种情况,这里面就包括当前重载我们运算符的示例,但没有必要将其作为一般工具来介绍。对于那些出现的应用程序,我们只要知道可以通过引用指定返回值,就可以使用与引用的函数相同的语法:我们只需在结果类型之后添加一个&符号即可。因此我们可以这样理解这个代码:

ostream & operator<=(ostream & os, Point pt);
1
1
将函数operator<= 中的返回的值,以引用的方式传递到输出流 ostream中。而且通过引用返回流,我们菜可以在周围的上下文中再次使用。所以,我们可以通过下列代码来重载我们的 << 运算符。

ostream & operator<<(ostream & os, Point pt) {
return os << pt.toString();
}
1
2
3
1
2
3
其他功能的重载

那么,既然可以重载插入运算符,那么我们是不是也可以重载其他类型的运算符呢?答案是肯定的。我们给Point类设计一些其他运算符。例如,给定两个点p1和p2,您可以通过应用 == 运算符来测试这些点是否相等,就像使用字符串或者数值一样。
这里C++提供了两种用于重载内置操作符的策略,以便它与新定义的类的对象一起工作。

你可以将运算符定义为类中的一种方法。 当您使用此样式重载运算符时,左操作数是接收器对象,右操作数作为参数传递。(就像上面的例子)
你可以将运算符定义为类外的自由函数。 如果使用此样式,则运算符的操作数都将作为参数传递
基于方法的运算符重载

如果使用基于(1)的样式,则扩展==操作符的第一步是将此运算符的原型添加到point.h接口,如下所示:

bool operator==(Point rhs);
1
1
该方法是Point类的一部分,因此必须将其定义为其公共部分的一部分(public)。 相应的实现出现在point.cpp文件中,其实现代码如下:

bool Point::operator==(Point rhs) {
return x == rhs.x && y == rhs.y;
}
1
2
3
1
2
3
与通过接口导出的类的一部分的其他方法一样,operator ==的实现必须通过在方法名称之前添加Point ::前缀来指定它与Point类相关联。所以我们可以这样使用这个方法:

if (pt == origin) ....
1
1
假设pt和origin都是Point类型的变量,当遇到这个表达式时,编译器会从Point类调用==运算符。由于operator ==是一种方法,编译器将变量pt指定为接收方,然后将origin的值复制给参数rhs。在运算符==方法的主体中,对整个x和y的引用是指变量pt中的成员,而rhs.x和rhs.y表达式是指变量origin中的成员。
operator ==方法的代码提供了面向对象编程的重要属性的有用说明。operator ==方法显然可以访问当前对象的x和y字段,因为类中的任何方法都可以访问自己的私有变量。 可能更难理解的是,该变量持有完全不同的对象,operator ==方法也可以访问rhs的私有变量吗?没错,这些引用在C++中是合法的,因为类的private部分中的定义对类是私有的,而不是对象。类的方法的代码可以引用该类的任何对象的实例变量。

基于自由函数的运算符重载

我们容易发现基于方法的运算符重载很容易使得我们混淆,因为编译器对待操作数是不同的。它指定一个作为接收方,另一个作为参数的做法常常让人分不太清楚。恢复对称性的最简单的方法是使用将运算符定义为自由函数的替代方法。 使用此策略,则point.h接口需要包含以下原型:

bool operator==(Point p1, Point p2);
1
1
这个原型声明一个自由函数,因此必须出现在Point类的定义之外。 相应的实现不再包含Point ::前缀,因为运算符不再是类的一部分,其实现看起来像这样:

bool operator==(Point p1, Point p2) {
return p1.x == p2.x && p1.y == p2.y;
}
1
2
3
1
2
3
虽然这个实现更容易去理解,因为它对称地对待参数p1和p2。但是这个代码有一个重大的问题:它实际上并不工作。 如果将这个定义添加到Point类中,你的代码甚至不会编译。问题的关键在于==运算符现在被定义为一个空闲函数,因为它无法访问私有实例变量x和y(private)。
但是C++可以以另一种方式解决访问问题。假设==运算符出现在point.h接口中,它在概念上与Point类相关联,因此在某种意义上应该具有对类的私有变量的访问权限。
为了使此设计工作,Point类必须让C++编译器知道对于特定函数(在这种情况下为==操作符的重载版本)可以查看其私有实例变量。为了使这种访问合法,Point类必须将operator ==函数指定为朋友(friend)。在这种情况下,友谊与社交网络具有同等的特征,比如您的私人信息通常不会与整个社区共享,但你可以允许你的朋友访问。
在C++中,将一个自由函数声明为朋友(国内很多书籍称为友元函数,我个人觉得就是朋友,没必要那么拗口)的语法如下:

friend prototype;
1
1
其中原型(prototype)是完整的函数原型。在这里,例如,你要指定自由函数operator ==是通过写入Point类的一个朋友函数。你可以这样写:

friend bool operator==(Point p1, Point p2);
1
1
当然,C++中不仅仅一个函数可以作为类中的朋友,类与类之间也可以有朋友的关系,语法如下:

friend class name;
1
1
name就是被指定的朋友的类的名字。在C++中,这种友谊的声明不是自动相互的。如果两个类都希望访问另一个类的私有变量,那么每个类都必须将其他类显式声明为一个朋友。(就好比,两个人是朋友的条件是,他把你当朋友,你也把他当朋友,而不是单方面的认为他是你朋友)。
每当你重载一个类的==运算符时,最好为!=运算符提供重载的定义。 毕竟,客户会期望他们能够测试两点是否不同,因为它们可以测试这些点是否相同。 C++不假定 == 和!=返回相反的值。如果你想要这种行为,你必须分别重载这些运算符。但是,您可以在实现operator!=时使用operator ==,因为operator ==是该类的公共成员。因此,最直接的实现就是这样:

bool operator!=(Point p1, Point p2) {
return !(p1 == p2);
}

C++抽象编程·运算符重载与友元函数的更多相关文章

  1. c++入门之—运算符重载和友元函数

    运算符重载的意义是:将常见的运算符重载出其他的含义:比如将*重载出指针的含义,将<<与cout联合使用重载出输出的含义,但需要认识到的问题是:运算符的重载:本质仍然是成员函数,即你可以认为 ...

  2. C++运算符重载(友元函数方式)

    我们知道,C++中的运算符重载有两种形式:①重载为类的成员函数(见C++运算符重载(成员函数方式)),②重载为类的友元函数. 当重载友元函数时,将没有隐含的参数this指针.这样,对双目运算符,友元函 ...

  3. C++ 学习笔记(五)类的知识小结一(重载,友元函数,静态成员,new)

    ---恢复内容开始--- 学习C++类知识点还是挺多的,每个知识点学习的时候都觉得这个知识点咋那么多东西,其实真学完了再回头看好像也就那么点.这次用程序写一个黑猫揍白猫的故事总结一下这段时间学习的零碎 ...

  4. C++运算符重载形式——成员函数or友元函数

    运算符重载是C++多态的重要实现手段之一.通过运算符重载对运算符功能进行特殊定制,使其支持特定类型对象的运算,执行特定的功能,增强C++的扩展功能. 运算符重载的我们需要坚持四项基本原则: (1)不可 ...

  5. 初探C++运算符重载学习笔记&lt;2&gt; 重载为友元函数

    初探C++运算符重载学习笔记 在上面那篇博客中,写了将运算符重载为普通函数或类的成员函数这两种情况. 以下的两种情况发生.则我们须要将运算符重载为类的友元函数 <1>成员函数不能满足要求 ...

  6. C++运算符重载(成员函数方式)

    一.运算符重载 C++中预定义的运算符的操作对象只能是基本数据类型,实际上,对于很多用户自定义类型,也需要有类似的运算操作.如果将C++中这些现存的运算符直接作用于用户自定义的类型数据上,会得到什么样 ...

  7. c++运算符重载和虚函数

    运算符重载与虚函数 单目运算符 接下来都以AClass作为一个类例子介绍 AClass{ int var } 区分后置++与前置++ AClass operator ++ () ++前置 一般设计为返 ...

  8. C++多态性----运算符重载与虚函数

    一.多态性 ①概述:多态是指同样的消息被不同类型的对象接收时导致的不同行为. ②类型: 可以分为四类:重载多态.强制多态.包含多态.参数多态. ------------------------ --- ...

  9. CPP_运算符重载及友元

    运算符重载 两种重载方法1)成员函数 a + b => a.operator+(b); 一个参数 2)友元函数 a + b => operator+(a, b); 两个参数. friend ...

随机推荐

  1. 前端魔法堂:解秘FOUC

    前言  对于问题多多的IE678,FOUC(flash of unstyled content)--浏览器样式闪烁是一个不可忽视的话题,但对于ever green的浏览器就不用理会了吗?下面尝试较全面 ...

  2. 自己实现的string的库函数

    为了更好地理解string的各个库函数,现将几个常用的库函数用自己的方式实现如下: #include<iostream> using namespace std; #include< ...

  3. STM32之呼吸灯实验

    首先,我想引用一下在一片博文里 看到 的一段话,写的很详细, 首先来说,你要使用PWM模式你得先选择用那个定时器来输出PWM吧!除了TIM6.TIM7这两个普通的定时器无法输出PWM外,其余的定时器都 ...

  4. CF #349 div1 B. World Tour

    题目链接:http://codeforces.com/problemset/problem/666/B 大意是给一张有向图,选取四个点,使得走这四个点,任意两个点之间走最短路,总距离最长. 3000个 ...

  5. linux基础 作业篇

    1.自动部署反向代理 web nfs #!/usr/bin/python #-*- coding:utf-8 -*- #开发脚本自动部署及监控 #1.编写脚本自动部署反向代理.web.nfs: #!/ ...

  6. Openstack Swift 原理、架构与 API 介绍

    OpenStack Swift 开源项目提供了弹性可伸缩.高可用的分布式对象存储服务,适合存储大规模非结构化数据.本文将深入介绍 Swift 的基本设计原理.对称式的系统架构和 RESTful API ...

  7. javascript 中 with 的使用

    1)简要说明         with 语句可以方便地用来引用某个特定对象中已有的属性,但是不能用来给对象添加属性.要给对象创建新的属性,必须明确地引用该对象. 2)语法格式  with(object ...

  8. [刷题]算法竞赛入门经典(第2版) 5-3/UVa10935 - Throwing cards away I

    书上具体所有题目:http://pan.baidu.com/s/1hssH0KO 代码:(Accepted,0 ms) //UVa10935 - Throwing cards away I #incl ...

  9. Spring-AOP用法总结

    前言     Spring AOP的实现方法很多,在项目开发中具体采用什么方式,需要按实际情况来选择,每一种的用法,有其一定的实用价值,所以本文将各种使用方法进行了具体实现.主要包括Advice的be ...

  10. php中for循环的应用1

    for 循环是 PHP 中最复杂的循环结构.它的行为和 C 语言的相似.在PHP中使用的是执行相同的代码集的次数. for 循环的语法是: for (expr1; expr2; expr3)state ...