一文深入理解C++运算符的重载

一文帮助深入理解C++运算符的重载

声明Rational有理数类

运算符重载是C++的一个十分有意思的特性,能使除了基本变量之外的,其他的C++类声明也能实现基本的运算,下面将从一个有理数类Rational的实现与扩展,在这个类的深入设计过程中思考运算符的运用,以达到深入理解C++运算符重载的目的,有理数Rational相比于double能更精确地表示一个数据,实现一个Rational类能提供一种精确表达的方法, 但是Rational并不是C++的基本类型,需要我们手动实现,下面为Rational用例图的定义

Rational用例图

上述样例图声明了Rational的方法,其代码实现为

// Rational.h
class Rational{
public:
    Rational();										// 初始化一个默认有理数
    Rational(int n, int d);							// 根据传入的分子分母初始化一个有理数
    Rational(int n);								// 传入一个整数n,构建n/1有理数
    int getNumerator() const;						// 获取分子
    int getDenominator() const;						// 获取分母
    Rational add(const Rational &secR) const;		// 有理数相加
    Rational subtract(const Rational &secR) const;	// 有理数相减
    Rational multiply(const Rational &secR) const;	// 有理数相乘
    Rational divide(const Rational &secR) const;	// 有理数相除
    int compareTo(const Rational &secR) const;		// 有理数比较,大于返回1,等于返回0,小于返回-1
    bool equals(const Rational &secR) const;		// 判断两个有理数是否相等
    int intValue() const;							// 返回有理数的int值,向下取整
    double doubleValue() const;						// 返回有理数的双精度值
    std::string toString() const;					// 返回有理数的字符串描述
private:
    int numerator;                          		// 分子
    int denominator;                        		// 分母
    static int gcd(int n, int d);           		// 求n和d的最大公约数,用于化简为最简形式
};
// Rational.cpp
#include "Rational.h"
#include <sstream>
#include <cstdlib>
#include <assert.h>

// default constructor
Rational::Rational():numerator(0), denominator(1) {}

// constructor
Rational::Rational(int n, int d) {
    assert(d != 0);
    int factor = Rational::gcd(n, d);
    this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
    this->denominator = abs(d)/factor;
}

Rational::Rational(int n):numerator(n), denominator(1) {}

// return the value of the numerator
int Rational::getNumerator() const {
    return this->numerator;
}

// return the value of the denominator
int Rational::getDenominator() const {
    return this->denominator;
}

// add two rational and return the result
Rational Rational::add(const Rational &secR) const {
    int n = numerator * secR.getDenominator() + denominator * secR.getNumerator();
    int d = denominator * secR.getDenominator();
    return Rational(n, d);
}

// subtract two rational and return the result
Rational Rational::subtract(const Rational &secR) const {
    int n = numerator * secR.getDenominator() - denominator * secR.getNumerator();
    int d = denominator * secR.getDenominator();
    return Rational(n, d);
}

// multiply two rational and return the result
Rational Rational::multiply(const Rational &secR) const {
    int n = numerator * secR.getNumerator();
    int d = denominator * secR.getDenominator();
    return Rational(n, d);
}

// divide two rational and return the result
Rational Rational::divide(const Rational &secR) const {
    int n = numerator * secR.getDenominator();
    int d = denominator * secR.getNumerator();
    return Rational(n, d);
}

// Find GCD of two numbers
int Rational::gcd(int n, int d) {
    int n1 = abs(n);
    int n2 = abs(d);
    int gcd = 1;
    for(int k = 1 ; k <= n1 && k <= n2; k++){
        if(n1 % k == 0 && n2 % k == 0)
            gcd = k;
    }
    return gcd;
}

/**
 * @param secR
 * @return:
 *      this > secR -> 1
 *      this = secR -> 0
 *      this < secR -> -1
 */
int Rational::compareTo(const Rational &secR) const {
    Rational rad = subtract(secR);
    if(rad.getNumerator() > 0)
        return 1;
    else if(rad.getNumerator() < 0)
        return -1;
    return 0;
}

// compare two rational judge if the value are equal
bool Rational::equals(const Rational &secR) const {
    Rational rad = subtract(secR);
    if(rad.getNumerator() == 0)
        return true;
    return false;
}

// return the int value of the rational
int Rational::intValue() const {
    return numerator/denominator;
}

// return the double value of the rational
double Rational::doubleValue() const {
    return 1.0 * numerator / denominator;
}

// to string
std::string Rational::toString() const {
    std::stringstream ss;
    ss << numerator;

    if(denominator > 1)
        ss << '/' << denominator;

    return ss.str();
}

上述代码已经实现了一个比较全面的有理数类,能够做很多的事情,比如加、减、乘、除、比较、转换为double、toString等等,已经可以说是很全面了,比如下面的例子

Rational r1(2, 3), r2(5, 8), r3;
r3 = r1.add(r2); // r3 = r1 + r2

从上面的例子中,首先定义了两个有理数r1,r2,然后他们在相加将得到的结果赋值给·r3,但是有问题的一点是,这样的加法似乎不太满足我们日常的惯例写法,能不能直接写成r3 = r1 + r2的形式?答案肯定是可以的,下面我们就来一个个的实现运算符的重载,并分析运算符重载过程中需要注意的事情

一步一步实现各种运算符的重载

C++是支持运算符的重载的,但不是所有运算符可以重载的,可以重载的运算符如下,并且相同的运算符进行不同的重载它所代表的含义是不同的

+-*/%^&|~!=
<>+=-=*=/=%=^=&=|=<<
>><<=>>===!=<=>=&&||++
->*,->[]()newdelete

上面的运算符是可以重载的,而下面的运算符是不可以重载的

?:..*::

Note:

  • C++本身定义了运算符的结合性和优先级,重载运算符并不能改变运算符的结合性和优先级

运算符重载本质上是一个函数,只是其调用的方式跟平常的函数调用a.fun()不同,比如前面的有理数类实现了+加法运算符,**那么r1 + r2等价于r1.operator+(r2)**是不是跟函数调用很像?下面就着手实现这些运算符,并从中分析其中的一些需要注意的点

重载加减乘除

+,-,*,/…这些都是二元运算符,顾名思义二元运算符需要两个操作数,加法需要一个加数,和一个被加数,这就是二元的,这里是需要注意的是,如果说一个类重载了+运算符,他不一定是重载了加运算符,因为+它本身不止一个含义,因为它既可以作为二元运算符r1+r2,也可以是一元运算符+r1,他们的重载方式是不一样的,这些二元运算符的声明如下:

// 声明
Rational operator   +   (const Rational &secR) const;
Rational operator   -   (const Rational &secR) const;
Rational operator   *   (const Rational &secR) const;
Rational operator   /   (const Rational &secR) const;
// 实现
Rational Rational::operator+(const Rational &secR) const {
    return add(secR);
}

Rational Rational::operator-(const Rational &secR) const {
    return subtract(secR);
}

Rational Rational::operator*(const Rational &secR) const {
    return multiply(secR);
}

Rational Rational::operator/(const Rational &secR) const {
    return divide(secR);
}

有了上面的重载,我们就可以在代码中直接这样写了

r3 = r2 + r1;  // <==> r3 = r2.operator+(r1);

重载[]

C++的数组, map, vector,他们都支持[]运算符,那么能不能让Rational也支持[]呢,让r1[0]返回分子,r1[其他整数]返回分母?答案显而易见,完全可以的!

// 重载[]运算符
int operator[](int index);

// 实现
int Rational::operator[](int index) {
    if(index == 0)
        return numerator;
    return denominator;
}

经过上面的重写,我们也已经可以调用[]来访问有理数的分子分母了!但是,map,vector等都支持[]访问并且可以通过[]来修改其中的值,而上面的重载能不能通过[]来改变分子分母的值呢?答案是不可以的,因为从上面的声明可以看到,返回的是一个int类型,即返回的是以分子或分母的一个右值,而右值是用于赋值的,这样的声明不会对分子分母的值造成影响,如果要支持修改特性,或者不仅仅返回值,我们还要拿到这个属性的修改权的话,我们需要对返回值做一些手脚

// 重载[]运算符
int& operator[](int index);

// 实现
int& Rational::operator[](int index) {
    if(index == 0)
        return numerator;
    return denominator;
}

当返回值变成引用的话,就可以实现修改属性值的效果,这也是在重载过程中需要注意的一个点

重载()

重载了()的类一般有另一种别称,仿函数,实现了这个运算符的类,可以像调用函数一样调用这个类,为什么要实现这个运算符?因为这类仿函数相对于真正的函数来说,不仅仅能实现函数的功能,而且能持有自己的特性和状态

void operator()();


void Rational::operator()(){
    printf("I am a rational!\n");
}

重载简写运算符

简写运算符是类似于+=, *=, -=, /=这类的运算符,他的重载方式也十分简单,但是也需要注意与前面[]运算符同样的问题,一般这类运算符是可以重复调用的,比如

int x = 3;
x += 3 += 3; // 这样是合理的,要更明确一点的话 (x += 3)+= 3

从上面可以看出,这类运算符的重载的返回值也是有考究的,也应该是引用类型,需要我们在重载的时候要注意这个细节

Rational& operator+=(const Ratioanl& secR);


Rational& Rational::operator+=(const Rational& secR){  
    *this = add(secR);
    return *this;
}

有了上面的声明与重载,那么下面的测试代码会输出

Rational r1(2, 4);
Rational r2 = r1 += Rational(2, 3);
cout<<"r1 is "<<r1.toString()<<endl;
cout<<"r2 is "<<r2.toString()<<endl;
r1 is 7/6
r2 is 7/6

重载一元运算符

正如之前提到的+这个运算符它不仅是二元运算符r1 + r2,还可以是一元运算符+r2,而这类运算符怎么声明?其格式如下,比如说+ -两个一元运算符的声明

Rational operator+();
Rational operator-();
Rational Rational::operator-() {
    return Rational(-numerator, denominator);
}

Rational Rational::operator+() {
    return Rational(numerator, denominator);
}

在一元运算符中,有两个比较特殊的++和--,这两个运算符有前缀和后缀一说, 一般前缀运算符是左值的,而后缀运算符则不是,前缀运算符的重载返回值为引用类型,并且两者的重载声明也有区别,如下

// 前缀 Prefix
Rational& operator++();

// 后缀 PostFix
Rational operator++(int dummy);

// 实现
Rational& Rational::operator++() {
    int n = numerator + denominator;
    int d = denominator;
    int factor = Rational::gcd(n, d);
    this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
    this->denominator = abs(d)/factor;
    return *this;
}

Rational& Rational::operator--() {
    int n = numerator - denominator;
    int d = denominator;
    int factor = Rational::gcd(n, d);
    this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
    this->denominator = abs(d)/factor;
    return *this;
}

Rational& Rational::operator++(int) {
    int n = numerator + denominator;
    int d = denominator;
    int factor = Rational::gcd(n, d);
    this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
    this->denominator = abs(d)/factor;
    return *this;
}

Rational& Rational::operator--(int) {
    int n = numerator - denominator;
    int d = denominator;
    int factor = Rational::gcd(n, d);
    this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
    this->denominator = abs(d)/factor;
    return *this;
}

用友元函数重载流运算符<<, >>

C++允许重载>><<流运算符,但是这些运算符必须以非成员的形式重载,也就是说一般以友元函数的形式重载流运算符,其声明形式为

friend std::ostream& operator<<(std::ostream& out, const Rational& secR);

friend std::istream& operator>>(std::istream& in, Rational& secR);

// 实现
std::ostream& operator<<(std::ostream &out, const Rational &secR){
    out<<secR.numerator<<"/"<<secR.denominator;
    return out;
}

std::istream& operator>>(std::istream &in, Rational &secR){
    int n,d;
    in>>n>>d;
    secR = Rational(n, d);
    return in;
}

经过以上的声明,我们就可以通过coutcin来输出和输入有理数类了

Rational r1;
cin>>r1;
cout>>r1;
5 2
5/2

自定义类型转换

前面也提到,intValue和doubleValue能将一个有理数分别转换成一个int值和一个double值,那能不能自动转换呢?而且int能不能自动转换成Rational呢?答案当然是可以滴!

首先来看Rational自动转换成intdouble

// 声明
operator double();
operator int();

// 实现
Rational::operator double(){
    return doubleValue();
}

Rational::operator int(){
    return intValue();
}
Rational r1(3,2);
int a = 1 + r1; // r1 就被自动转换为int了,a = 2

然后再看int转换成Rational,可以通过添加一个构造函数的形式来实现自动转换,可以声明并实现Rational(int n)构造函数来实现。

Rational(int n):numerator(n), denominator(1){}
Rational r1(2,3), r2;
r2 = r1 + 1;  // r2 = 5/3

前面的表达式r2 = r1 + 1其实本质上是r2 = r1.operator+(1),而因为实现了这个构造函数,则会隐式地调用Rational(int n)这个构造函数,前提是Rational(int n)后面没有explict但是这里有一个很容易忽视的致命问题,如果上述两种转换都声明与实现了的话。那么会导致两种转换都编译不通过,因为会导致歧义,所以最好只保留一个,假如doubleint和int到Rational的类型转换都有了,那么r1 + 1可能会被翻译成

int + int
double + int
Rational + Rational
.....

从而造成歧义!这里一定要注意

总结

关于运算符重载,笔者个人的总结有,我们需要知道

  • 哪些运算符可以重载,哪些运算符不能重载
  • 要注意区分一元运算符和二元运算符,因为有些是有同一个运算符会有不同的涵义,比如+
  • 即使是一元运算符,有的可能有前缀和后缀之分,他们的重载也有区别,比如++和--
  • 要注意重载的运算符是不是左值运算符,即返回值要不要用引用,要用引用的有[] ++和--的前缀表示....
  • 流运算符要用非成员函数重载(即友元函数)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值