函数运算符重载

0.如何重载函数运算符

三种方法:friend function、common function以及member function,下面一一阐述

1.挑个简单的入手

假设我们需要定义一个类型表示分数,简单定义如下·:

class fraction{
public:
    fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){

    }

private:
    int numerator_;
    int denominator_;
};

那么我们需要给这个类型定义一些简单的运算,例如加减乘除等,定义加法操作需要重载operator+operator+是一个binary的运算符,他接受两个参数,返回一个fraction的新对象。
我们先使用友元函数来重载operator+

//fraction.h
class fraction{
public:
    fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){

    }
    friend fraction operator+(const fraction& lhs, const fraction& rhs);
private:
    int numerator_;
    int denominator_;
};
fraction operator+(const fraction& lhs, const fraction& rhs);
//fraction.cpp
fraction operator+(const fraction &lhs, const fraction &rhs) {
    int demonimator=lhs.denominator_*rhs.denominator_;
    int numerator=lhs.numerator_*rhs.denominator_+rhs.numerator_*lhs.denominator_;
    int gcd=tiny_utils::gcd(numerator,demonimator);
    if(gcd==0){
        return fraction(0,1);
    }
    else{
        return fraction(numerator/gcd,demonimator/gcd);
    }
}

要注意friend函数在类内仅仅是指定该函数为类的友元函数,并不是函数的声明,所以我们要在下方再次声明这个函数,然后源文件中完成定义。虽然经测试,gcc不必进行这个声明,但是最好还是声明一下,这样代码在所有编译器上都能通过。friend函数也可以直接在类型声明,不过这种函数最好还是遵守声明与定义分离的原则。
我们也可以使用common function实现operator+,实现大致差不多,如下所示:

//fraction.h
class fraction{
public:
    fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){

    }
    const int& getNumerator()const {
        return numerator_;
    }
    const int& getDenominator()const {
        return denominator_;
    }
    //friend fraction operator+(const fraction& lhs, const fraction& rhs);
private:
    int numerator_;
    int denominator_;
};
fraction operator+(const fraction& lhs, const fraction& rhs);
//fraction.cpp
fraction operator+(const fraction &lhs, const fraction &rhs) {
    int demonimator=lhs.getDenominator()*rhs.getDenominator();
    int numerator=lhs.getNumerator()*rhs.getDenominator()+rhs.getNumerator()*lhs.getDenominator();
    int gcd=tiny_utils::gcd(numerator,demonimator);
    if(gcd==0){
        return fraction(0,1);
    }
    else{
        return fraction(numerator/gcd,demonimator/gcd);
    }
}

由于common function不能直接访问类的private成员,所以我们需要定义两个getter函数,其实他和friend function也就差一个访问权限。
我们再使用member function来实现这个函数,实现member function时,我们就不需要两个参数了,因为操作符左边的operand已经由this提供,我们仅需要提供操作符右边的operand。

//fraction.h
class fraction{
public:
    fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){

    }
    const int& getNumerator()const {
        return numerator_;
    }
    const int& getDenominator()const {
        return denominator_;
    }
    fraction operator+(const fraction& other);
private:
    int numerator_;
    int denominator_;
};
//fraction.cpp
fraction fraction::operator+(const fraction &other) {
    int demonimator=denominator_*other.getDenominator();
    int numerator=numerator_*other.getDenominator()+other.getNumerator()*denominator_;
    int gcd=tiny_utils::gcd(numerator,demonimator);
    if(gcd==0){
        return fraction(0,1);
    }
    else{
        return fraction(numerator/gcd,demonimator/gcd);
    }
}

上面用三种不同的方式实现了operator+的重载,三者差别不大,我们接着看。

2.member function的局限性

对于分数计算来说,我们常常会将一个整数和一个分数来进行四则运算,那么此时上面重载的operator+就不够用了,此时我们需要重载新的函数以满足我们的要求,例如分数和整数运算或者整数和分数运算。同样我们先用friend function来实现:

//fraction.h
#define friend_func
class fraction{
public:
    fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){

    }
    const int& getNumerator()const {
        return numerator_;
    }
    const int& getDenominator()const {
        return denominator_;
    }

#ifdef mem_func
    fraction operator+(const fraction& other);
#else
    friend fraction operator+(const fraction& lhs, const fraction& rhs);
    friend fraction operator+(const fraction& lhs,const int rhs);
    friend fraction operator+(const int lhs,const fraction& rhs);
#endif
private:
    int numerator_;
    int denominator_;
};
#ifndef mem_func
fraction operator+(const fraction& lhs, const fraction& rhs);
fraction operator+(const fraction& lhs,const int rhs);
fraction operator+(const int lhs,const fraction& rhs);
#endif
//fraction.cpp
fraction operator+(const fraction &lhs, const fraction &rhs) {
    int demonimator=lhs.denominator_*rhs.denominator_;
    int numerator=lhs.numerator_*rhs.denominator_+rhs.numerator_*lhs.denominator_;
    int gcd=tiny_utils::gcd(numerator,demonimator);
    if(gcd==0){
        return fraction(0,1);
    }
    else{
        return fraction(numerator/gcd,demonimator/gcd);
    }
}
fraction operator+(const fraction &lhs, const int rhs) {
    fraction rhsf(rhs,1);
    return lhs+ rhsf;
}

fraction operator+(const int lhs, const fraction &rhs) {
    fraction lhsf(lhs,1);
    return lhsf+ rhs;
}

对于member函数,我们没有办法满足这个要求,所以说一旦我们需要定义不同类型之间的binary运算,friend function是更好的选择,当然你也可以选择common function,随你的便😄。
在定义完operator+之后,我们可以作为依照,将operator-operator/operator*都定义出来。

3.看看运算结果吧

当我们完成运算时,最想看到的莫过于他的结果,所以如何将结果友好地输出出来就成了下一项工作,也就是我们喜闻乐见的operator<<operator<<也是一个binary的操作符,他接受一个std::ostream&作为左边的operand,因为std::ostream的拷贝构造函数是delete的,所以这里必须使用引用,又因为我们要向流中输出一些东西,所以不能是const引用。返回类型这里不再像operator+那样使用std::ostream,而是使用std::ostream&,一是因为std::ostream是不可拷贝的,二是这样方便我们进行链式调用,简而言之就是out<<f1<<" "<<f2。你当然可以在这里使用void作为返回类型,那么你就不能链式调用了。除非你跟用户有仇,想要折磨他,那么你最好还是不要使用void,你高兴就好😄。下面是一个简单的operator<<重载,使用friend function实现,对于其他实现方法,就不一一赘述了。

//fraction.h
#define friend_func
class fraction{
public:
    fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){

    }
    const int& getNumerator()const {
        return numerator_;
    }
    const int& getDenominator()const {
        return denominator_;
    }

#ifdef mem_func
    fraction operator+(const fraction& other);
#else
    friend fraction operator+(const fraction& lhs, const fraction& rhs);
    friend fraction operator+(const fraction& lhs,const int rhs);
    friend fraction operator+(const int lhs,const fraction& rhs);
    friend std::ostream& operator<<(std::ostream& out,const fraction& rhs);
#endif
private:
    bool isValidFraction()const {
        return denominator_!=0;
    }
private:
    int numerator_;
    int denominator_;
};
#ifndef mem_func
fraction operator+(const fraction& lhs, const fraction& rhs);
fraction operator+(const fraction& lhs,const int rhs);
fraction operator+(const int lhs,const fraction& rhs);
std::ostream& operator<<(std::ostream& out,const fraction& rhs);
#endif
//fraction.cpp
std::ostream& operator<<(std::ostream &out, const fraction &rhs) {
    if(rhs.isValidFraction()){
        if(rhs.numerator_%rhs.denominator_)
            out<<rhs.numerator_<<"/"<<rhs.denominator_;
        else
            out<<rhs.numerator_;
    }
    else{
        out<<"invalid fraction";
    }
    return out;
}

我们也可以用同样的方式定义出operator>>,这里不再赘述。

4.再来个++吧

对于内置类型intiterator等,标准提供了operator++operator--的操作。如果你想说这就是两个函数,我可是有前置++和后置++的,别急,标准库都为我们定义好了,看下表:

操作名 语法 类内定义 类外定义
前自增 ++a T& T::operator++(); T& T::operator++(T& a);
后自增 a++ T T::operator++(int); T T::operator++(T& a,int);
前自减 --a T& T::operator--(); T& T::operator--(T& a);
后自减 a-- T T::operator--(int); T T::operator--(T& a,int);

其实前后自增的区别就是多了一个int参数,这个参数只有区分前后自增的作用,没有其他任何作用,在调用时默认会传入0,如果你闲的蛋疼,也可以通过operator++(1)把他设成1,那么我猜你跟前面用void做返回类型的是同一种人😅。
从上表的类内定义来看,前置操作返回的是operand的引用,后置操作返回的是操作数的一个prvalue,你可以理解为一个右值。两者内在的区别就是前置操作的返回值严格等于operator+=或者operator-=,而后者进行自增后返回的是自增前的值。在了解了这些后,我们要重载operator++就很轻松了。而且只要你把上面的都看懂,遇到一些白痴问题例如:int a=0;++(i++);的结果是什么的时候你就能应答自如。很显然上面的白痴表达式不能通过编译,因为你不能将一个右值赋给一个左值引用,除非他是const的😂。
但是如何为分数定义一个自增操作呢?我们应该在自增后让分数的值加一还是只让分母加一,前者显然更合理,但是如果一个操作不够明显或者容易让人产生误会,那么你最好在声明的地方加上注释,或者干脆直接不重载该运算符。这里我们实现一个让值加一的版本:

//declaration
//pre increasement
fraction& operator++(fraction& operand);
//post increasement
fraction operator++(fraction& operand,int flag);
//implementation
fraction &operator++(fraction &operand) {
    operand.numerator_+=operand.denominator_;
    return operand;
}

fraction operator++(fraction &operand, int flag) {
    fraction prvalue=operand;
    operand.numerator_+=operand.denominator_;
    return prvalue;
}

很简单,对于自减操作也同样如此,不再赘述。

5.顺便说一下operator+=

前面提到前置自增的结果严格等于operator+=(1),我们根据这个来定义一个operator+=操作,这个重载你同样可以像operator+那样指定不同的类型,因为他也是一个binary的运算符,简单定义两个:

//declaration
fraction& operator+=(fraction& lhs, const fraction& rhs);
fraction& operator+=(fraction& lhs, const int rhs);
//implementation
fraction &operator+=(fraction &lhs, const fraction &rhs) {
    int demonimator=lhs.denominator_*rhs.denominator_;
    int numerator=lhs.numerator_*rhs.denominator_+rhs.numerator_*lhs.denominator_;
    int gcd=tiny_utils::gcd(numerator,demonimator);
    if(gcd==0){
        lhs.numerator_=0;
        lhs.denominator_=1;
    }
    else{
        lhs.numerator_=numerator/gcd;
        lhs.denominator_=demonimator/gcd;
    }
    return lhs;
}

fraction &operator+=(fraction &lhs, const int rhs) {
    fraction rhsf(rhs,1);
    return operator+=(lhs,rhsf);
}

operator+不同的是,我们这里也选择了返回一个引用,这不仅可以避免不必要的拷贝,还为我们的fraction类提供了类似于内置int型的链式调用,例如:(f1+=2)+=3;当然你得在调用时加上括号表示优先级,否则运算符优先级会先调用2+=3,两个int&&作为操作数,这与我们重载的任何operator+=都不符合,无法通过编译。事实上,你也无法定义一个两个参数类型都是内置类型的运算符重载,假设编译器允许这样做,在你调用两个内置类型进行运算时,到底应该选择哪个函数来调用呢?

6.比较一下大小吧

在定义好数值运算的运算符重载后,对于一个分数而言,比较他们的大小也是极其重要的操作。可以重载的比较操作符由==!=>>=<<=以及<=>,其中最后一个operator<=>C++20新标准增加的three-way comparator,它接受两个参数a和b,如果a>b,那么返回一个正值,如果a==b,那么返回0,a<b则返回一个负数。比较运算符也是binary的运算符,返回值为bool。下面定义一个简单的operator==

//declaration
bool operator==(const fraction& lhs, const fraction& rhs);
//implementation
bool operator==(const fraction &lhs, const fraction &rhs) {
    if(!lhs.isValidFraction()||!rhs.isValidFraction())
        return false;
    int lGcd=tiny_utils::gcd(lhs.numerator_,lhs.denominator_);
    int rGcd=tiny_utils::gcd(rhs.numerator_,rhs.denominator_);
    return ((lhs.numerator_/lGcd)==(rhs.numerator_/rGcd))&&
        ((lhs.denominator_/lGcd)==(rhs.denominator_/rGcd));
}

std::unordered_set<T>等关联容器默认使用operator<作为Compare的模板参数,但是在我们在定义了比较运算符后,我们仍然不能将fraction用在关联容器上,还需要为fraction定义一个`function object用作哈希函数,可以参考我的另一篇文章如何在关联容器中使用自定义类型

6.与内置类型的交互

对于一个分数,如果我们像将他存储为一个double类型的数该怎么做,一种办法就是提供一个public的方法const double getDoubleValue()const,我们在这个方法里面提供一个计算。当然你也可以提供一个private的成员变量double double_val_,在构造分数时就完成浮点运算。这看上去有点蠢,因为如果你对该分数进行了上面所讲的自增或者自减操作,你还得重新计算浮点数。
还有一种办法就是重载operator double,不过我建议你最好慎重考虑,因为一旦定义了类型运算符重载,你的代码可能会产生意想不到的结果。对于这里的情况,如果我们定义了operator double,那么我们就绝对不能再定义接受double类型的运算符重载了,因为你定义了也没用,编译器会把fraction隐式转换成double,然后用double执行内置的算数操作😄。一般来说,除了operator bool,我们不应该定义类型转换操作符,如果非要定义,最好定义显式的类型转换运算符:

explicit operator double ()const {
    return static_cast<double >(numerator_)/denominator_;
}

7.完了

关于常用的操作符重载就差不多是这样了,operator[]等不常用的就不写了,主要我也没用过这些😄。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • C++运算符重载-上篇 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符和二元逻辑运算符4...
    Haley_2013阅读 2,291评论 0 51
  • C++运算符重载-下篇 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符和二元逻辑运算符4...
    Haley_2013阅读 1,435评论 0 49
  • 友元函数 作用:普通函数通过友元可以访问一个类的私有或者保护数据,以提高效率 运算符重载 ​ 虚函数 用于多态 纯...
    Hassan_chao阅读 367评论 0 0
  • 基本上我们进行运算符重载时有两种形式,类内的运算符重载和顶层函数位置的运算符重载。 操作符重载指的是将C++提供的...
    飞扬code阅读 1,669评论 0 4
  • 我甘心微笑着等你,许我四海为家,倾一世温柔,暖你所有的薄凉 我不知道,还能陪你走出多远!我岁月的青山,开始有星星点...
    主父眼阅读 326评论 0 0