当前位置: 首页 > 编程日记 > 正文

C++拾趣——有趣的操作符重载

操作符重载是C++语言中一个非常有用的特性。它可以让我们比较优雅的简化代码,从而更加方便的编写逻辑。

为什么要使用操作符重载

一种常见的用法是重载<<运算符,让标准输出可以输出自定义的类型。比如

#include <iostream>class Sample {friend std::ostream& operator<<(std::ostream &out, const Sample& smp);
private:int _m = 0;
};std::ostream& operator << (std::ostream &out, const Sample& smp) {out << smp._m;return out;
}int main() {Sample sample;std::cout << sample << std::endl; // output 0return 0;
}

Sample是非标准类型,编译器并不知道怎么使用标准输出输出它——是输出它的内存结构还是输出它的一些变量呢?这个时候Sample类的作者通过重载<<运算符,告知编译器“我”是想输出它的某个成员变量。(转载请指明出于breaksoftware的csdn博客)

如果我们不进行重载,则可能像如下代码实现功能

#include <iostream>class Sample {
public:int value() {return _m;}
private:int _m = 0;
};int main() {Sample sample;std::cout << sample.value() << std::endl; // output 0return 0;
}

这种写法,需要对每个需要输出的成员变量定义一个“访问器”,“访问器”的个数随着需要输出的成员变量个数线性增加。假如“访问器”只有在标准输出时才使用,且不存在需要单独输出某些变量的场景,这种实现就显得不那么“智慧”——大量“访问器”函数的重用次数太低了。

有人会提出可以定义一个类似print的函数来实现

#include <iostream>class Sample {
public:void print() {std::cout << _m;}
private:int _m = 0;
};int main() {Sample sample;sample.print();std::cout << std::endl; // output 0return 0;
}

这种写法亦不那么“智慧”。这给试图输出组合信息的使用者带来麻烦。本来一行可以输出类的信息和换行符,在上例中就需要写两行。这种逻辑的“割裂”是不优雅的。

可能有人会说:虽然我认同操作符重载是优雅的,但是这样的“教学例子”仍然让我无法感知到它的重要性。是的,因为例子太简单。以一个工作中的场景为例:

工作中经常会用到Json或者XML等格式的数据,一般情况下,我们都需要将这些格式转换成一个对象来访问。假如我们不太清楚该格式的具体组织形式以及字段名称或者值类型,难道我们要一个个遍历整个对象么?这个时候,以“肉眼”可以看懂的格式输出该对象就显得非常必要了。可能有人会说Json和XML的内容是可以肉眼识别的。的确,但是如果该数据是一种二进制的结构呢?

重载操作符需要遵从“隐性共识”

C++给了程序员很多自由,但是自由永远都是相对的。因为重载操作符是存在一些隐性的共识,这些共识是我们要遵从的,否则将失去操作符重载的意义,甚至会给使用者带来极大的困扰。

隐性的共识包含几个部分:

  • 符合自然语义。比如我们重载操作符=,隐性的共识是该操作符将会产生赋值行为。而如果我们什么都不去实现,则违反了共识。再比如,我们重载++操作符,其隐性的共识是需要对关键信息进行自增。如果我们实现时,让关键信息自减了,那也是明显违反共识的。
  • 操作符存在关联性。关联性又分为:对等性和复合性。下面我们将针对这两个特性进行讨论。

自增和自减操作符是对等,它们都会对对象的关键信息作出修改。但是对等性要求我们,如果自增是对关键信息增加1,那么自减就是对该信息减少1。不可以产生一次自增,需要几次自减才会恢复到原始值的现象。

复合性是指:+操作和+=操作,*操作和*=操作……这种存在组合关联的操作符。比如我们实现了+操作符的重载,也就需要实现+=的重载。因为我们无法保证别人不去使用+=去进行“加”和“赋值”的操作。对于一对操作符,一般来说,我们让两者实现存在协同关系,即+使用+=实现,或者+=使用+和=实现。看个例子

class Sample {
public:Sample& operator=(const Sample& smp) {_m = smp._m;return *this;}   Sample operator+(const Sample& smp) {Sample tmp(*this);tmp += smp;return tmp;}   Sample& operator+=(const Sample& smp) {_m += smp._m;return *this;}
private:int _m = 0;
}

上例中我们使用+=实现了+操作。这儿一个有趣的点是第4行,我们直接使用了smp._m——_m可是私有变量啊。其实不用担心,因为smp也是Sample对象,且这个重载是Sample类的成员函数,所以在语法上是合法的。

自增、自减的前置和后置

自增(++)和自减(--)是非常独特的单目运算符,它可以出现在操作数的前面或者后面。如果出现在前面,则隐性共识是:自增(减)关键信息,并返回自身;如果出现在后面,则隐性共识是:自增(减)关键信息,返回自增(减)之前的自身。其一般实现是:构造一个和自身相同的临时对象,自增(减)关键信息,返回临时对象。

之前有一种与此相关的面试题。面试官会:A和B两者写法,哪个执行更高效?

// A
for (int i = 0; i < 8; i++) {}// B
for (int i = 0; i < 8; ++i) {}

这个问题就是考察后置自增(减)会构造临时对象的知识点。但是就此例子来看,这个问题构造的并不好。因为现在的编译器已经比较智能了,它会识别该场景不需要构造临时变量,于是A编译出的指令和B编译出的指令是一致的,执行效果也是一样的。

由于自增和自减是对等的,简单起见,之后的讨论我只以自增为例。

问题来了:

  • 前置和后置是否需要分开实现?由于两者执行逻辑不同,我们不可能通过重载一个操作符实现另外一个功能,所以这个答案是“是”。
  • 是否只需要重载前置或者后置?如果我只重载前置,那么使用者只能在使用前置操作符时才能产生正确的行为,但是使用者不知道后置是不能使用的。这种不对等的行为也是违反“隐性共识”的。所以这个问题的答案是“否”。
  • 前置和后置是同一个操作符,如何在重载声明上表现出区别?这个问题的答案就是C++的一个语法糖,也是本文标题中“有趣”的一个点。

C++使用了一种语法糖来区分前置和后置——前置重载无参数,后置重载有一个int型参数。看个例子

class Sample {
public:Sample& operator++() {std::cout << "prefix ++" << std::endl;;++_m;return *this;}   Sample operator++(int n) {std::cout << "postfix ++" << n << std::endl;Sample tmp(*this);++*this;return tmp;}
private:int _m = 0;
}

第3行是前置实现,它只是简单的对成员变量进行了自增,然后返回对象本身。第9行是后置实现,它在自增前使用了拷贝构造函数构造了一个和当前对象保存一样信息的临时对象,然后自增当前对象,最后返回了临时对象。

在进行后置操作符调用时,如果没有指定参数,系统会默认传入0。所以第9行,n的值默认是0。

介于这种语法,我们还可以如下调用前置操作

    sample.operator++();

或者这样调用后置操作。然传入的是10,系统也的确把10传入了重载函数,但是我们不应该去使用它。因为这只是C++的一个无可奈何的语法糖。

    sample.operator++(10);

再回到之前的面试题,如果面试官询问++sample和sample++哪个效率高些时,你则可以告知是前置高些,因为后置方式使用了拷贝构造函数构造了一个临时对象。

&&、||的短路求值特性

除了自增、自减具有“前置”或者“后置”区别外,还有一组操作符——&&和||具有特殊的属性——短路求值。假如我们重载&&或者||操作符,则没法保证该特性,而它却是“隐性共识”。

if (ptr && ptr->suc()) {// do somethind
}

上例中,我们希望先检测ptr是否为空,然后再调用suc方法。因为默认的&&支持短路求值,所以如果ptr为空,则整个判断结果为假,那么suc函数不会被执行。

if (ptr->value() > 10 || ptr->value() < -10) {// do something
}

||操作的短路求值是:从左向右,只要遇到一个条件为真的,则整个判断为真。之后的检测不用执行了。所以如果ptr->value()值是20,那么只会判断20是否大于10(因为已经为真),而不会去判断是否小于-10。

但是重载这两个操作符就会破坏短路求值特性。比如

#include <iostream>class Sample {friend std::ostream& operator<<(std::ostream &out, const Sample& smp);friend bool operator&&(bool pre, const Sample& smp);friend bool operator||(bool pre, const Sample& smp);
public:Sample& operator++() {std::cout << "prefix ++" << std::endl;;++_m;return *this;}Sample operator++(int n) {std::cout << "postfix ++" << n << std::endl;Sample tmp(*this);++*this;return tmp;}bool operator&&(const Sample& smp) {return _m && smp._m;}bool operator||(const Sample& smp) {return _m || smp._m;}private:int _m = 0;
};std::ostream& operator << (std::ostream &out, const Sample& smp) {out << smp._m;return out;
}bool operator&&(bool pre, const Sample& smp) {return pre && smp._m;
}bool operator||(bool pre, const Sample& smp) {return pre || smp._m;
}int main() {Sample* sample = NULL;std::cout << "sample && (*sample) && (*sample)++ " << (sample && (*sample) && (*sample)++) << std::endl;return 0;
}

这个程序的执行结果是

postfix ++0
Segmentation fault

最后它崩了。如果按照短路求值特性,由于sample为空,则整个运算结果为假。但是重载&&操作符后,(*sample)++被执行,从而将导致违例。

再看看||的操作

Sample* sample = new Sample;
std::cout << "sample || (*sample) || (*sample)++ " << (sample || (*sample) || (*sample)++) << std::endl;

它的输出是

postfix ++0
prefix ++
sample || (*sample) || (*sample)++ 1

如果按照短路求值,由于sample不为空,则整个运算结果为真。但是重载了||操作符后,短路求值特性丢失,于是要把所有||的操作都执行一遍(最后执行了自增操作)。

(非)成员函数和隐式构造

操作符重载可以定义为外部函数(因为可能会访问私有变量,所以常常被声明为友元),也可以定义为成员函数。

以二目操作符为例。如果操作符重载被定义为成员函数,则重载函数的参数(如果有的话)是操作符右侧值。因为成员函数隐藏了this指针,所以操作符左侧值就是this指针指向的对象。

如果定义为外部函数,则函数的两个参数分别为操作符的左、右值。

#include <iostream>class Sample {friend std::ostream& operator<<(std::ostream &out, const Sample& smp);friend Sample operator+(const Sample& smpL, const Sample& smpR);
public:Sample() {}Sample(int n) : _m(n) {}Sample operator+(const Sample& smpR) {Sample tmp(*this);tmp += smpR;return tmp;}Sample& operator+=(const Sample& smp) {_m += smp._m;return *this;}
private:int _m = 0;
};std::ostream& operator << (std::ostream &out, const Sample& smp) {out << smp._m;return out;
}Sample operator+(const Sample& smpL, const Sample& smpR) {return Sample(smpL._m + smpR._m);
}

上面例子第14行是加法的成员函数式的重载,第33行是友元式的重载。

这两种实现是有区别的,区别就是对隐式构造函数的处理。

如果只有成员函数式重载,则下面的调用方式可以工作。因为操作符左侧值是Sample对象。

Sample sample;
sample = sample + 2;

但是下面的代码不能编译通过,因为左侧值是个整型。

sample = 2 + sample;

如果想解决这个问题,就可以将加法重载设置为外部形式。这样编译器会将2隐式构造成一个Sample临时对象(调用Sample(int n)构造函数)。

但是如果隐式构造成本比较大,比较建议的方案是明确化,比如

Sample operator+(int n, const Sample& smpR) {return Sample(n + smpR._m);
}

但是不是所有重载都可以设置为成员函数形式,比如上面例子中频繁出现的<<重载。因为它用于支持标准输出,于是操作符左侧值是std::ostream对象,这样它就不能声明为成员函数了。

也不是所有重载都可以设置为外部函数形式,比如赋值(=)、下标([])、调用(())等。

函数对象

函数很容易理解,但是函数对象是什么?

下面是一般函数调用,函数名是some_method,它有两个参数,返回了一个type类型数据。

type a = some_method(arg1, arg2);

我们将注意力移到括号(())上,它是一个操作符。因为C++提供了“操作符重载”这样的武器,我们是不是可以将some_method想象成某个类?一种方式是

class Method {                                                                                                      
public:int operator ()(int n, int m) const {return n * m;}
};int main() {Method m;std::cout << m(3, 4) << std::endl;std::cout << Method()(4, 5) << std::endl;return 0;
}

相较于第10行和第11行,第10行的调用方式更像普通的函数调用,但是它有一个缺点:需要显式的申明一个函数对象。第11行构造了一个临时对象——它没有名字,但是连续两个()让人感觉还是很“异类”。

一种比较优雅的方式是:

class Method {
public:Method(int n, int m) : _n(n), _m(m) {}operator int() const {return _n * _m;}private:Method() {}private:int _n = 0;int _m = 0;
};int main() {std::cout << Method(2, 3) << std::endl;return 0;
}

这儿用到了转换操作符的概念。我们使用“operator 类型()”的形式定义一个转换操作,这样该类对象可以直接转换成type类型。

“操作符重载”给我们提供了强大的工具,使我们可以编写出便于使用的类。但是它也藏着各种语法糖,通过本文,希望朋友们可以了解到它一些好玩的“糖”。

相关文章:

urlparse模块(专门用来解析URL格式)

# -*- coding: utf-8 -*- #python 27 #xiaodeng #urlparse模块&#xff08;专门用来解析URL格式&#xff09;#URL格式&#xff1a; #protocol ://hostname[:port] / path / [;parameters][?query]#fragment #parameters&#xff1a;特殊参数&#xff0c;一般用的很少。#1、url…

使用Boost的Serialization库序列化STL标准容器

使用Boost做对象序列化是非常方便的&#xff0c;本文将介绍一种序列化STL标准容器的方法。这是之前设计的异步框架的一个子功能&#xff1a;过程A将标准容器数据序列化成二进制流&#xff0c;然后将该二进制数据发送到过程B&#xff0c;过程B将数据反序列化为标准容器。&#x…

连登GitHub TOP榜,中国开发者在行动!

作者 | 唐小引数据 | 于瑞洋出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;中国开发者正在走向世界中文开源项目正在不断登上 GitHub TOP 榜不久前&#xff0c;一个名叫「wuhan2020」的开源项目进入了 GitHub Trending TOP 榜&#xff0c;截至到现在&#xff0c;已经…

Merge into的使用

用途 merge 命令可以用来用一个表中的数据来修改或者插入到另一个表。插入或者修改的操作取决于on子句的条件。该语句可以在同一语句中执行两步操作&#xff0c;可以减少执行多条insert 和update语句。merge是一个确定性的语句&#xff0c;即不会在同一条merge语句中去对同一条…

PHP和MySQL Web开发从新手到高手,第8天-创建categories管理页面

1. 创建categories管理页面 主要包含以下几个页面: A. index.php, 准备各种变量数据.供展示页面使用. B. categories.html.php, 显示categories. C. form.html.php, 用于编缉或添加作者的页面. 页面郊果: 2. categories页面的主要流程 2.1 是否已登录 if (!user_is_login()){in…

堆状态分析的利器——valgrind的DHAT

在《堆问题分析的利器——valgrind的massif》一文中&#xff0c;我们介绍了如何使用massif查看和分析堆分配/释放的问题。但是除了申请和释放&#xff0c;堆空间还有其他问题&#xff0c;比如堆空间的使用率、使用周期等。通过分析这些问题&#xff0c;我们可以对程序代码进行优…

cisco2950交换机密码恢复

在实际工作中可能会忘记交换机密码&#xff0c;导致不能对交换机进行配置的情况。cisco提供了密码恢复的方法。以下是关于交换机密码恢复说明&#xff1a;如果忘记密码&#xff0c;这时我们如果要配置交换机就必须在启动时绕过config.text的配置【密码保存在config.text中】然后…

程序员SQL都不会?能干啥?资深研发:别再瞎努力了!

国外有人曾做过调查显示&#xff1a;“SQL的使用人数仅次于JavaScript”。更有统计&#xff0c;世界上一流的互联网公司中&#xff0c;排名前 20 的有 80% 都是 MySQL 的忠实用户。为什么这项技术仍有这么多人在用&#xff1f;又为什么值得我们学习&#xff1f;1、无论你是前端…

OC管理文件方法

1、常见的NSFileManager文件方法 -(NSData *)contentsAtPath:path  //从一个文件读取数据 -(BOOL)createFileAtPath: path contents:(NSData *)data attributes:attr  //向一个文件写入数据 -(BOOL)removeItemAtPath:path error:err  //删除一个文件 -(BOOL)moveItemAtPa…

堆状态分析的利器——gperftools的Heap Profiler

在《内存泄漏分析的利器——gperftools的Heap Checker》一文中&#xff0c;我们介绍了如何使用gperftools分析内存泄漏。本文将介绍其另一个强大的工具——Heap Profiler去分析堆的变化过程。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 我们使用类似于《堆…

亲戚称呼关系表

直系血亲父系曾曾祖父--曾祖父--祖父--父亲曾曾祖母--曾祖母--祖母--父亲母系曾曾外祖父--曾外祖父--外祖父--母亲曾曾外祖母--曾外祖母--外祖母--母亲儿子&#xff1a;夫妻间男性的第一子代。女儿&#xff1a;夫妻间女性的第一子代。孙&#xff1a;夫妻间的第二子代&#xff0…

技术驰援抗疫一线, Python 线上峰会免费学!

截至截止2月9号24时&#xff0c;新型冠状病毒在全国已确诊42714例&#xff0c;疑似病例已达21675例。而专家所说的“拐点”始终未至&#xff0c;受疫的影响&#xff0c;各大公司开启远程办公模式&#xff0c;将返回工作场所办公的时间一延再延。在抗疫前线&#xff0c;中国医疗…

ZeroMq实现跨线程通信

ZeroMq实现跨线程通信 之前在技术崇拜的技术经理指导下阅读了ZeroMq的基础代码&#xff0c;现在就将阅读的心得与成果记录一下&#xff0c;并重新模仿实现了一下经理的异步队列。 1、对外接口 //主要接口&#xff08;1&#xff09;void *ymq_attach (void *ctx_, int oid, voi…

动态执行流程分析和性能瓶颈分析的利器——gperftools的Cpu Profiler

在《动态执行流程分析和性能瓶颈分析的利器——valgrind的callgrind》中&#xff0c;我们领略了valgrind对流程和性能瓶颈分析的强大能力。本文将介绍拥有相似能力的gperftools的Cpu Profiler。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 我们依然以callg…

C语言内存管理内幕(二)----半自动内存管理策略

2019独角兽企业重金招聘Python工程师标准>>> C语言内存管理内幕(二&#xff09;----半自动内存管理策略 转载于:https://my.oschina.net/hengcai001/blog/466

无人机巡逻喊话、疫情排查、送药消毒,抗疫战中机器人化身钢铁战士!

整理 | 夕颜责编 | Carol出品 | CSDN&#xff08;ID:CSDNnews&#xff09;这场抗疫战争似乎格外漫长&#xff0c;但回头细数一下才发现&#xff0c;自疫情爆发以来&#xff0c;也不过半月之久。在接下来的几个半月中&#xff0c;抗疫战仍将继续&#xff0c;各方力量也要绷紧神经…

jQuery EasyUI 表单插件 - Datebox 日期框

为什么80%的码农都做不了架构师&#xff1f;>>> 扩展自 $.fn.combo.defaults。通过 $.fn.datebox.defaults 重写默认的 defaults。 日期框&#xff08;datebox&#xff09;把可编辑的文本框和下拉日历面板结合起来&#xff0c;用户可以从下拉日历面板中选择日期。在…

互斥量、读写锁长占时分析的利器——valgrind的DRD

在进行多线程编程时&#xff0c;我们可能会存在同时操作&#xff08;读、写&#xff09;同一份内存的可能性。为了保证数据的正确性&#xff0c;我们往往会使用互斥量、读写锁等同步方法。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 互斥量的用法如下 pth…

一次性同步修改多台linux服务器的密码

如何一次性修改多台linux服务器的密码&#xff0c;这是个问题&#xff0c;我给大家提供一个脚本&#xff0c;是前一段我刚刚写的&#xff0c;希望能对大家有所帮助一 , 需求:linux环境下运行&#xff0c;需要tcl和expect支持原理说明&#xff1a;利用expect的摸拟交互的功能&…

麻省理工学院的新系统TextFooler, 可以欺骗Google的自然语言处理系统及Google Home的音频...

来源 | news.mit编译 | 武明利责编 | Carol出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;两年前&#xff0c;Google的AI还不太成熟。一段时间以来&#xff0c;有一部分计算机科学研究一直致力于更好地理解机器学习模型如何处理这些“对抗性”攻击&#xff0c;这些攻…

Oracle VS DB2 数据类型

Oracle VS DB2 本文转自&#xff1a;http://www.bitscn.com/oracle/install/200604/16541.html首先&#xff0c;通过下表介绍ORACLE与DB2/400数据类型之间的对应关系&#xff0c;是一对多的关系&#xff0c;具体采用哪种对应关系&#xff0c;应具体问题具体分析。 OracleDB2/40…

死锁问题分析的利器——valgrind的DRD和Helgrind

在《DllMain中不当操作导致死锁问题的分析--死锁介绍》一文中&#xff0c;我们介绍了死锁产生的原因。一般来说&#xff0c;如果我们对线程同步技术掌握不牢&#xff0c;或者同步方案混乱&#xff0c;极容易导致死锁。本文我们将介绍如何使用valgrind排查死锁问题。&#xff08…

疫情可视化,基于知识图谱的AI“战疫”平台如何做?

来源 | DataExa渊亭科技武汉封城半个月&#xff0c;疫情依然严峻。但与17年前的SARS相比&#xff0c;我国在此次疫情防控工作中展现出了更高的医疗救治水平、更快的防疫反应速度、更透明的信息披露机制、更迅速的数据报送机制。在这场没有硝烟的战役中&#xff0c;社会各界团结…

mysql乐观锁总结和实践

2019独角兽企业重金招聘Python工程师标准>>> 上一篇文章《MySQL悲观锁总结和实践》谈到了MySQL悲观锁&#xff0c;但是悲观锁并不是适用于任何场景&#xff0c;它也有它存在的一些不足&#xff0c;因为悲观锁大多数情况下依靠数据库的锁机制实现&#xff0c;以保证操…

数据竞争(data race)问题分析的利器——valgrind的Helgrind

数据竞争&#xff08;data race&#xff09;是指在非线程安全的情况下&#xff0c;多线程对同一个地址空间进行写操作。一般来说&#xff0c;我们都会通过线程同步方法来保证数据的安全&#xff0c;比如采用互斥量或者读写锁。但是由于某些笔误或者设计的缺陷&#xff0c;还是存…

sql charindex函数

CHARINDEX函数返回字符或者字符串在另一个字符串中的起始位置。CHARINDEX函数调用方法如下&#xff1a; CHARINDEX ( expression1 , expression2 [ , start_location ] ) Expression1是要到expression2中寻找的字符中&#xff0c;start_location是CHARINDEX函数开始在expressi…

170亿参数加持,微软发布史上最大Transformer模型

来源 | 微软译者 | 刘畅出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;Turing Natural Language Generation&#xff08;T-NLG&#xff09;是微软提供的一个有170亿参数的语言模型&#xff0c;在许多NLP任务上均优于目前的SOTA技术。我们向学者演示了该模型&#xf…

iOS 开发 OC编程 数组冒泡排序.图书管理

// // main.m // oc -5 数组 // // Created by dllo on 15/10/28. // Copyright (c) 2015年 dllo. All rights reserved. // #import <Foundation/Foundation.h> #import "Student.h" #import "Book.h" int main(int argc, const char * argv[])…

C#中使用Monitor类、Lock和Mutex类来同步多线程的执行(转)

C#中使用Monitor类、Lock和Mutex类来同步多线程的执行 在多线程中&#xff0c;为了使数据保持一致性必须要对数据或是访问数据的函数加锁&#xff0c;在数据库中这是很常见的&#xff0c;但是在程序中由于大部分都是单线程的程序&#xff0c;所以没有加锁的必要&#xff0c;但是…

从0开始搭建编程框架——思考

需求来源于问题。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 之前有个人做前端开发的同学在群里问“C语言能做什么&#xff1f;能写网页么&#xff1f;”&#xff0c;然后大家就开始基于这个问题展开争辩。有的认为是“不能&#xff0c;从来没听说过C语言…