【C++学习】运算符重载:自己写一个分数类

前言

相必读者您在编程生活中,应该会想过让自己的程序能够使用分数的运算、输出;也许命令行不方便实现像在纸上那样的自然书写显示,但是如同1/23/4这样的显示,或许也足以使自己感到一点成就感。

现在学习了C++,有了运算符重载等方便的功能,让我们能够方便的实现并使用分数的功能。

之前老师布置过一个【多项式的处理】的作业,奈何其仅支持正整数次数、小数系数;也是出于对它进行一个升级、让他能够支持分数的目的,我便决定开始写这样一个分数类。

这里有几篇博客,虽然我这里可能并没有实现提到的功能,但是它们帮助了我很多,欢迎大家浏览参考:

C++分数类 - 拱垲 - 博客园
C语言将循环小数/有限小数转换为分数 - CSDN
小数转化为分数 - 小白的个人总结 - 博客园

您也可以在搜索引擎中搜索相应的关键字,寻找您喜爱的内容。

2020/8/9 更新

  • 代码中的求最大公因数、最小公倍数的函数可以采用__gcd()__lcm(),这两个模板函数貌似是GNU的函数,需要包含“algorithm”头文件。详情可见这篇博客__gcd()
  • 也可以采用C++17提供的std::gcd()std::lcm()
  • 据一位朋友指点,还可尝试位运算优化的gcd,可以在搜索引擎中检索“gcd”+“位运算”等关键字获取相关内容。
  • 放几个链接:
    GCD最大公约数
    二进制GCD算法解析
    笔试面试中常见的位运算用法

设计

我希望这个分数类大概能够满足这些功能:

fraction a, b;
cin >> a >> b;
cout << a + b;
a += 1;
a -= b;
a = a + 5;
if (a > b) cout << a;
else cout << b;

总结一下大概就是:基本四则运算、输入输出、大小判断;那么类的声明大概长这个样子:

class fraction
{
    
    
private:
    int A;  //numerator
    int B;  //denominator
public:
    fraction();
    fraction(int );
    fraction(int ,int );
    fraction(const char* );
    
    void reduce();
    void print();
    bool if_int() const;

    friend istream& operator>> (istream&, fraction&);
    friend ostream& operator<< (ostream&, const fraction&);
    
    void add(const fraction& );
    void add(int );
    void subtract(const fraction& );
    void multiply(const fraction& );
    void divide(const fraction& );
    double value() const;

    //使 分数 与 分数 之间的运算成立
    fraction operator+ (const fraction& );
    fraction operator- (const fraction& );
    fraction operator* (const fraction& );
    fraction operator/ (const fraction& );
    
    void operator+= (const fraction& );
    void operator-= (const fraction& );
    void operator*= (const fraction& );
    void operator/= (const fraction& );
    
    //使 分数 与 整数 之间的运算成立
    fraction operator+ (int );
    fraction operator- (int );
    fraction operator* (int );
    fraction operator/ (int );
    
    void operator+= (int );
    void operator-= (int );
    void operator*= (int );
    void operator/= (int );

    friend fraction operator+ (int, const fraction& );
    friend fraction operator- (int, const fraction& );
    friend fraction operator* (int, const fraction& );
    friend fraction operator/ (int, const fraction& );

    //使大小判断成立
    int compare(const fraction& );
    bool operator== (const fraction& );
    bool operator!= (const fraction& );
    bool operator> (const fraction& );
    bool operator< (const fraction& );
    bool operator>= (const fraction& );
    bool operator<= (const fraction& );

    bool operator== (int);
    bool operator== (double );
    bool operator!= (int );
    bool operator> (int );
    bool operator< (int );
    bool operator>= (int );
    bool operator<= (int );
};

笔者用两个整数,分别存储分子和分母,由此得以保存一个分数(存储时,分数将被约分至最简);如果需要,可以使用long long int型数据。

最大公约数、最小公倍数

由于操作过程中,我们可能需要求分子分母的最大公约数、最小公倍数,所以写两个函数:

int gcd(int x,int y) //辗转相除法求最大公因数
{
    
    
	int t;
	while (y!=0) {
    
    
		t=x%y;
		x=y;
		y=t;
	}
	return x;
} //'__gcd()'也是采用的欧几里得算法
int lcm(int a, int b)
{
    
    
	//return a*b/gcd(a,b);
    //if (a % b == 0) return a; //这两句话可能会有一定的优化效果吧
    //if (b % a == 0) return b; //不是很清楚……
    if (a == 0 || b == 0) return 0; //看了'__lcm()'发现自己没做判断
    int t_gcd = gcd(a,b);
    if (a % t_gcd == 0) a /= t_gcd;
    else if (b % t_gcd == 0) b /= t_gcd;
    return a * b;
}

可以把它们俩声明为成员函数。

对于求ab的最小公倍数,我放弃了直接用a*b/gcd(a,b)的方法,因为a*b可能会超出范围;若二者之一能被它们的最大公约数相除,先进行除法操作会好一点点。

好像忘了说明,笔者这个分数类仅保存分数约分过后的结果;另外,分母限定不能为0,这个在构造函数里进行判断处理;如果分数是一个整数,则分母为存储为1;分数的正负性仅取决于分子;

2020/8/9 更新:笔者表述能力不行,今天看到别人的表述,恰道出了自己的想法,摘录于此:
对于分数,有基本的几个规则:

  1. 正负号挂在分子上;
  2. 当分数表示0的时候,分子为0,分母为1;
  3. 分子分母必须达到最简,也就是没有1以外的公约数

当分数进行四则运算的时候,也是基于这三条的性质来进行化简;

来源:分数的表示以及计算(c++)

约分

为了进行约分时更方便,我们先保存分数的正负性,然后对分子分母取绝对值,在约分过后,统一把正负性加在分子上

void fraction::reduce()
{
    
    
    bool if_negative = false;
    if ((A < 0 && B > 0) || (A > 0 && B < 0)) //if (A * B < 0)
    {
    
    
        if_negative = true;
    }
    A = abs(A);
    B = abs(B);
    int d = gcd(A,B);
    A /= d;
    B /= d;
    if (if_negative) A = -A;
}

输出分数

和上面一样的思想,单独输出分数的正负;

void fraction::print()
{
    
    
    int tA = abs(A);
    if (A < 0) cout << " - ";
    if (tA >= B && tA % B == 0) cout << (tA/B);
    else if (tA == 0) cout<<0;
    else cout << tA << "/" << B;
}

今天看这段代码,发现好像写得有点繁琐,下边缩减一下:

ostream& operator<< (ostream& os, const fraction& f)
{
    
    
    int tA = abs(f.A);
    if (f.A < 0) os << " - ";
    if (tA % f.B == 0) os << (tA / f.B);
    else os << tA << "/" << f.B;
    return os;
}

判断分数是否是整数、返回浮点值

bool fraction::if_int() const
{
    
    
    return abs(A) % B == 0;
}
double fraction::value() const
{
    
    
    return (double) A / B;
}

构造函数

fraction::fraction(): A(0), B(1) {
    
    }
fraction::fraction(int a): A(a), B(1) {
    
    }
fraction::fraction(int a, int b)
{
    
    
    if (b == 0) {
    
    cout<<"No 0 as denominator!\n"; b = 1;}
    A = a;
    B = b;
    reduce();
}

笔者比较得意的是下面这个从字符串初始化的功能,它可以实现从如11.21/22.4/3.21/2.32.3/4.8等样式的字符串的初始化。

2020/8/9 (打脸)更新:其实这个并没有什么好得意的;另外,一些修改建议是采用string类以及一些配套的方法比如stod()find()等可能会增强不少可读性。

fraction::fraction(const char* str)
{
    
    
    A = 0; B = 1;
    char* sep = (char* )str;
    char* dpoint = (char* )str;
    while (*sep != '/' && *sep != '\0') sep++;
    if (*sep == '\0') //未找到分数线
    {
    
    
        while (*dpoint != '.'&&*dpoint != '\0') dpoint++;
        if(*dpoint == '\0') //没有小数点,即为一个整数
        {
    
    
            add( atoi(str) );
        }
        else 
        {
    
    
            int t1 = atoi(str);
            dpoint++;
            int t2 = atoi(dpoint);
            if (t1 < 0) t2 = -t2;
            int t3 = 1;
            while( *dpoint >= '0' && *dpoint <= '9') {
    
    t3*=10; dpoint++;}
            A = t2;
            //if (t1 < 0) A = -A;
            B = t3;
            reduce();
            add(t1);
        }
    }
    else //存在分数线
    {
    
    
        //处理分数线之前的部分,存入当前类
        while (*dpoint != '.' && *dpoint != '/') dpoint++;
        if(*dpoint == '/') //没有小数点,即为一个整数
        {
    
    
            add( atoi(str) );
        }
        else 
        {
    
    
            int t1 = atoi(str);
            dpoint++;
            int t2 = atoi(dpoint);
            if (t1 < 0) t2 = -t2;
            int t3 = 1;
            while( *dpoint >= '0' && *dpoint <= '9') {
    
    t3*=10; dpoint++;}
            A = t2;
            B = t3;
            reduce();
            add(t1);
        }
        //处理分数线之后的部分,
        fraction div;
        dpoint = sep;
        while (*dpoint != '.'&&*dpoint != '\0') dpoint++;
        if(*dpoint == '\0') //没有小数点,即为一个整数
        {
    
    
            div.add( atoi(sep+1) );
        }
        else 
        {
    
    
            int t1 = atoi(sep+1);
            dpoint++;
            int t2 = atoi(dpoint);
            if (t1 < 0) t2 = -t2; 
            int t3 = 1; //10^n
            while( *dpoint >= '0' && *dpoint <= '9') {
    
    t3*=10; dpoint++;}
            div.A = t2;
            div.B = t3;
            div.reduce();
            div.add(t1);
        }
        divide(div);
    }
}

流插入运算符重载

istream& operator>> (istream& is, fraction& f)
{
    
    
    char input[100];
    is >> input;
    f = fraction(input);
    return is;
}

加上、减去、乘上、除以

void fraction::add(const fraction& addend)
{
    
    
    if (B == addend.B) {
    
    
        A += addend.A;
        return;
    }
    else if (addend.B == 1) {
    
    
        A += (addend.A * B);
    }
    else {
    
    
        int t = lcm(B, addend.B);
        int m1 = t / B;
        int m2 = t / addend.B;
        A *= m1;
        B *= m1;
        A += (addend.A * m2);
        reduce();
    }
}
void fraction::add(int n)
{
    
    
    A += n*B;
}
void fraction::subtract(const fraction& subtrahend)
{
    
    
    if (B == subtrahend.B) {
    
    
        A -= subtrahend.A;
        return;
    }
    else if (subtrahend.B == 1) {
    
    
        A -= (subtrahend.A * B);
    }
    else {
    
    
        int t = lcm(B, subtrahend.B);
        int m1 = t / B;
        int m2 = t / subtrahend.B;
        A *= m1;
        B *= m1;
        A -= (subtrahend.A * m2);
        reduce();
    }
}
void fraction::multiply(const fraction& multiplier)
{
    
    
    int d1 = 1, d2 = 1;
    if (A != 0) d1 = gcd(A, multiplier.B);
    if (multiplier.A != 0) d2 = gcd(multiplier.A, B);
    A /= d1;
    B /= d2;
    A *= (multiplier.A / d2);
    B *= (multiplier.B / d1);
    //reduce();
}
void fraction::divide(const fraction& divisor)
{
    
    
    if (divisor.A == 0) {
    
    cout << "cannot divided by 0!\n"; return;}\
    int d1 = 1, d2 = 1;
    d1 = gcd(B,divisor.B);
    if (A != 0 && divisor.A != 0) d2 = gcd(A,divisor.A);
    A /= d2;
    B /= d1;
    A *= (divisor.B / d1);
    B *= (divisor.A / d2);
    //reduce();
}

其实上面这些函数的返回值可以设定成fraction&return *this;,实现连续调用。
上边的fraction::add(int)方法其实大概并不需要,因为有了fraction::fraction(int)这个构造函数之后,即便只有fraction::divide(const fraction &)a.divide(5)也是可以成立的。不过,专门为整数写一个重载,或许能减少从int生成类的时间,提高效率。

运算符+ - * / 的重载

可以直接调用之前写好的add()subtract()等方法,也可以重新写过;我就直接偷懒了:

fraction fraction::operator+ (const fraction& addend)
{
    
    
    fraction t(A,B);
    t.add(addend);
    return t;
}
fraction fraction::operator- (const fraction& subtrahend)
{
    
    
    fraction t(A,B);
    t.subtract(subtrahend);
    return t;
}
fraction fraction::operator* (const fraction& multiplier)
{
    
    
    fraction t(A,B);
    t.multiply(multiplier);
    return t;
}
fraction fraction::operator/ (const fraction& divisor)
{
    
    
    fraction t(A,B);
    t.divide(divisor);
    return t;
}

虽然我为int型的参数写了重载,但下面这样写还是相当于偷懒了,跟不写一样,读者可以进行一下优化:

fraction fraction::operator+ (int num )
{
    
    
    fraction addend(num);
    fraction t(A,B);
    t.add(addend);
    return t;
}
fraction fraction::operator- (int num )
{
    
    
    fraction subtrahend(num);
    fraction t(A,B);
    t.subtract(subtrahend);
    return t;
}
fraction fraction::operator* (int num )
{
    
    
    fraction multiplier(num);
    fraction t(A,B);
    t.multiply(multiplier);
    return t;
}
fraction fraction::operator/ (int num )
{
    
    
    fraction divisor(num);
    fraction t(A,B);
    t.divide(divisor);
    return t;
}

当整数在分数类前时,也要能够成立:

fraction operator+ (int num, const fraction& t)
{
    
    
    fraction p(num);
    p.add(t);
    return p;
}
fraction operator- (int num, const fraction& t)
{
    
    
    fraction p(num);
    p.subtract(t);
    return p;
}
fraction operator* (int num, const fraction& t)
{
    
    
    fraction p(num);
    p.multiply(t);
    return p;
}
fraction operator/ (int num, const fraction& t)
{
    
    
    fraction p(num);
    p.divide(t);
    return p;
}

运算符+= -= *= /=的重载

这些运算符恰好符合我们之前写好的add()subtract()等方法的逻辑,直接调用即可:

void fraction::operator+= (const fraction& addend)
{
    
    
    add(addend);
}
void fraction::operator-= (const fraction& subtrahend)
{
    
    
    subtract(subtrahend);
}
void fraction::operator*= (const fraction& multiplier)
{
    
    
    multiply(multiplier);
}
void fraction::operator/= (const fraction& divisor)
{
    
    
    divide(divisor);
}

下面同样是偷懒划水之作,写了等于不写,读者可以自行优化定义:

void fraction::operator+= (int num)
{
    
    
    fraction t(num);
    add(t);
}
void fraction::operator-= (int num)
{
    
    
    fraction t(num);
    subtract(t);
}
void fraction::operator*= (int num)
{
    
    
    fraction t(num);
    multiply(t);
}
void fraction::operator/= (int num)
{
    
    
    fraction t(num);
    divide(t);
}

大小比较

2020/8/9 更新:笔者在朋友的指点(批评)下以及后续的学习中得知,一般先定义小于<运算符、大于>运算符(或者小于<和等于==运算符),然后用他们返回值经过与、或运算来定义其他运算符。
其实朋友几个月前就批评我了,然后我鸽到现在(也没有改)

fraction::compare()有三种返回值:1表示当前分数大于参数传入的分数;-1表示当前分数较小;0表示两个分数相等:

int fraction::compare(const fraction& t)
{
    
    
    if (A * t.A < 0)
    {
    
    
        if (A < 0) return -1;
        if (t.A < 0) return 1;
    }
    if (A * t.B < t.A * B) return -1;
    else if (A * t.B == t.A * B) return 0;
    else return 1;
}

下面有些函数的定义总算是没有偷懒了:

bool fraction::operator== (const fraction& t) {
    
    
    return A * t.B == t.A * B;
    //return (A == t.A && B == t.B);
}
bool fraction::operator!= (const fraction& t) {
    
    
    return A * t.B != t.A * B;
}
bool fraction::operator> (const fraction& t) {
    
    
    if (compare(t) == 1) return true;
    else return false;
}
bool fraction::operator< (const fraction& t) {
    
    
    if (compare(t) == -1) return true;
    else return false;
}
bool fraction::operator>= (const fraction& t) {
    
    
    if(compare(t) != -1) return true;
    else return false;
}
bool fraction::operator<= (const fraction& t) {
    
    
    if(compare(t) != 1) return true;
    else return false;
}
bool fraction::operator== (int t) {
    
    
    return A == t * B;
}
bool fraction::operator== (double t) {
    
    
    return value() == t;
}
bool fraction::operator!= (int num) {
    
    
    return A != num * B;
}
bool fraction::operator> (int num) {
    
    
    return A > num * B;
}
bool fraction::operator< (int num) {
    
    
    return A < num * B;
}
bool fraction::operator>= (int num) {
    
    
    return A >= num * B;
}
bool fraction::operator<= (int num) {
    
    
    return A <= num * B;
}

使用

恰好有个作业题要把几个分数加起来,我就试了一下:

int main()
{
    
    
	fraction a,b;
	//fraction z(1); fraction x("10/11.9") ; fraction c("15/17.2") ; fraction v("5/13.7");
	fraction z,x,c,v;
	cin>>z>>x>>c>>v; 
	cout<<z<<" "<<x<<" "<<c<<" "<<v<<endl;
	a = x + z + c + v;
	cout<<a<<endl;
	fraction q("1/11.9") ; fraction w("1/17.2") ;fraction e("1/13.7");
	cout<<q<<" "<<w<<" "<<e<<" "<<endl;
	b = q + w + e;
	cout<<b<<endl;
	cout<<a/b;
	return 0;
}

也正是这么一用使我发现了自己的几个问题:一是通分算法不够优秀,也就是求最小公倍数的函数有些问题,会溢出;二是自己的函数的参数是fraction&而不是const fraction &,这样有个问题就是,类似下面的语句不会通过:

fraction a = fraction(1,2) + fraction(1,3);

我猜想的原因应该就是,通过类名加参数生成的临时类,不能被更改,所以只能用常引用。下面这个例子貌似也是这个理儿:

如果函数是这样定义的:

void some_function(string & a, string & b) {
    
    
    string s = a + b;
    cout << s;
}

那么下面这样使用就不行:

some_function("app","le");

解决方法

要么这样调用:

string a = "app";
string b = "le";
some_function(a,b);

或者将函数改为:

void some_function(string a, string b) {
    
     /*...*/ }

但最好还是这样定义函数:

void some_function(const string & a, const string & b)  {
    
     /*...*/ }

当然,也许这些本来应该都是常识的……

猜你喜欢

转载自blog.csdn.net/henry_23/article/details/106111371