我正在尝试学习清晰简洁的C ++。目前,我正在学校学习课程,并且为了练习,我开设了分数部分。

我非常感谢您提供有关样式,清洁度或其他任何很好了解的帮助。

(我知道using namespace std;很难理解,但是在我们的课程中是必需的。)

#include <iostream>
using namespace std;

class Fraction {
    int numerator, denominator;
private:
    int gcd(int a, int b) {
        while (a != b) {
            if (a > b)
                a -= b;
            else
                b -= a;
        }
        return a;
    }
public:
    Fraction (int, int);
    double decimal();
    string fraction();
    Fraction add(Fraction);
    Fraction subtract(Fraction);
    Fraction multiply(Fraction);
    Fraction divide(Fraction);
};

int main() {
    Fraction f = {2,3};
    cout << f.decimal() << endl;
    Fraction k = {1,4};
    Fraction d = {1,3};
    Fraction result = k.add(d).add(f);
    cout << result.fraction() << endl;
    return 0;
}

Fraction::Fraction(int n, int d) {
    numerator = n / gcd(n, d);
    denominator = d / gcd(n, d);

}
string Fraction::fraction() {
    return to_string(numerator) + "/" + to_string(denominator);
}
double Fraction::decimal() {
    return (double)numerator / (double)denominator;
}

Fraction Fraction::add(Fraction f) {
    return Fraction(numerator * f.denominator + f.numerator * denominator, denominator * f.denominator);
}
Fraction Fraction::subtract(Fraction f) {
    return Fraction(numerator * f.denominator - f.numerator * denominator, denominator * f.denominator);
}
Fraction Fraction::multiply(Fraction f) {
    return Fraction(numerator * f.numerator, denominator * f.denominator);
}
Fraction Fraction::divide(Fraction f) {
    return Fraction(numerator * f.denominator, denominator * f.numerator);
}


评论

这里的十进制函数是什么“十进制”?

@mvds它似乎返回小数的十进制/浮点值的事实:对于1/4为0.25。

@TripeHound返回一个(二进制)浮点数,代表0.25(十进制),但不等于“ 0.25”。 “十进制”一词通常表示以10为底,此处不涉及底数(2除外)。如果返回字符串或与基数10有关的某种数据类型,情况将有所不同。

@mvds我(略)明白你的意思:双精度不是“十进制”,但是“分数”与“十进制”似乎是一个足够普通的概念(加上浮点数几乎从未显示在任何东西上的事实除了base-10以外),调用函数十进制是合理的。我承认,像toDouble这样的技术在技术上可能更正确...

@TripeHound“足够普通了” ...这就是微妙的错误和技术债务进入现实世界代码的方式:了解编程但并不完全了解编程内容的程序员。如果添加了一个hex()函数,该函数返回一个字符串十六进制表示形式,binary()或octal(),将会发生什么?我想它们会很“普通”,并且会与原有的decimal()函数完全不匹配。强制转换运算符在这里会更适合。

#1 楼

您希望获得一些样式和清洁方面的建议,因此您可以在该方面进行一些改进:


您的课程中不要包括gcd,因为它实际上是完全无关的。而是为其编写一个单独的非类函数。
命名您的参数。诸如Fraction add(Fraction);之类的内容通常暗示该参数将被忽略,即使用户理解此处不是这种情况,仍不清楚参数实际代表什么。
包括所有使用的标头。您的代码当前缺少#include <string>
进行一些错误检查,并确保参数有效。例如,当有人将d的0传递给构造函数时会发生什么?您将陷入无限循环,因为您的gcd函数无法验证其参数是否为0。
避免C样式转换。 C ++提供了各种具有明确定义用途的转换,而C转换包含许多不同的转换,其中某些转换可能是您不希望的。在您的情况下,请不要写(double)numerator;改写static_cast<double>(numerator)(当然,与denominator相同)。
不要让addsubtractmultiplydivide代替,而应该重载相应的运算符(例如operator+add)。您还应该考虑添加更多运算符,例如operator+=operator-=等。
不要在简单的std::endl就足够了的情况下使用'\n'(几乎总是这样)。原因是std::endl还会刷新底层缓冲区,这在执行大量IO时会严重损害性能,并且很少需要。
fraction对于返回类的字符串表示形式的函数来说是一个坏名字。例如,您可以将其命名为to_string,甚至可以选择实现explicit operator std::string()
您可以放心地在return 0;的末尾省略main,编译器可以自动添加它。
当前,您的构造函数两次计算gcd(n, d)。您应该计算一次该值,然后将集合numerator = n / gcd_valuedenominator = d / gcd_value相除。

您应该考虑向构造函数的第二个参数中添加默认值1,以方便地从整数构造分数,即

Fraction::Fraction(int n, int d = 1);
...
Fraction f {3}; //f represents the fraction 3/1


不需要时,请勿将单个字符视为字符串。根据您的情况,将"/"中的'/'替换为fractionoperator+中有一​​个std::string可以处理单个char
修复Fraction (int, int)中不必要的空格。从定义其他方法的方式来看,它应该改为Fraction(int, int)(我想这只是一个错字)。


编辑:正如JPhi1618在评论中指出的那样,最好将值分配给对象时,不要养成依赖于隐式构造的习惯。相反,应该使用显式构造和统一初始化。因此,我将点11的代码示例的最后一行从Fraction f = 3;更改为其当前表示形式。

评论


\ $ \ begingroup \ $
谢谢!您的回答非常深入和有益。它对我有很大帮助。
\ $ \ endgroup \ $
–正在加载...
17-10-19在17:02

\ $ \ begingroup \ $
@loading ...非常欢迎!请询问您是否还有任何不清楚的地方。
\ $ \ endgroup \ $
–本·斯特凡(Ben Steffan)
17-10-19在17:02

\ $ \ begingroup \ $
还有std :: gcd。无需重新实现它。
\ $ \ endgroup \ $
–Rakete1111
17-10-19在18:24

\ $ \ begingroup \ $
@ Rakete1111我完全不知道。谢谢!
\ $ \ endgroup \ $
–本·斯特凡(Ben Steffan)
17-10-19在18:59

\ $ \ begingroup \ $
如何进行模板而不是int,以便我们可以使用long或long long的分数?那么理智地处理负分子和分母又如何呢?
\ $ \ endgroup \ $
– David Schwartz
17-10-19在22:40

#2 楼

Ben Staffen的好答案,只是想添加:


在传递复杂类型时,最好使用const引用。它可能与此类无关,但是随着类型的构造变得更加复杂/昂贵,const-refs会对性能产生巨大的影响。
默认情况下,使用类将成员设为私有,因此您拥有的第一个私有说明符不必要。


#3 楼

Ben Steffan建议用add代替operator+并添加operator+=。这样做是一个好主意,但是遵循以下特定模式是明智的:将operator+=实现为Fraction& Fraction::operator+=(Fraction)(即成员函数),并将operator+实现为自由函数

inline Fraction operator+(Fraction lhs, Fraction rhs) {
   lhs += rhs;
   return lhs;
}


由于这依赖于公众operator+=,所以不必成为朋友。

[编辑]
并对此进行扩展,mvds正确地怀疑decimal是正确的。通常将其拼写为operator double。您将要创建explicit,这样就不会隐式转换为double。例如。如果没有explicit,则std::cout << Fraction{4}将打印4.0000,因为它使用了隐式转换。

#4 楼

计算时不要害怕引入局部变量。例如,您的加法和减法函数有很长的一行。将它们写为以下内容会更清楚:

Fraction Fraction::add(Fraction f) {
    int addNumerator = numerator * f.denominator + f.numerator * denominator;
    int addDenomenator = denominator * f.denominator;
    return Fraction(addNumerator, addDenomenator);
}


这有两件事。它为中间值提供了有意义的名称,以便人们可以更轻松地进行计算。当您开始调试程序并逐行进行调试时,现在分别定义了计算的各个部分,因此可以轻松检查它们。任何现代的编译器都将足够聪明,可以在使用优化进行编译后删除局部变量的使用。

您有一个简单的计算和一个简短的类,因此这似乎有些多余,但更多复杂的情况可能会有所帮助。

评论


\ $ \ begingroup \ $
对于更多的Internet点,这些中间变量也应为const。
\ $ \ endgroup \ $
–user31517
17-10-20在20:40

\ $ \ begingroup \ $
@雪人,有趣。我认为我在实践中从未见过这种情况,编译器也将弄清楚这一点。
\ $ \ endgroup \ $
–JPhi1618
17-10-20在20:41

\ $ \ begingroup \ $
应该弄清楚了。我了解C ++的一件事是从不承担任何责任。
\ $ \ endgroup \ $
–user31517
17-10-20在20:42

#5 楼

除了其他答案之外,

使用const关键字强制不变性。这可以用于不更改对象状态的功能的签名。 double decimal() const;在c ++中传递对象(不会被函数改变)的首选方式是const reference例如。 Fraction add(const Fraction &f)。这比按值传递便宜,因为不需要复制构造对象。

只能在const对象上调用const函数。因此,通过声明一个函数const in,然后可以在诸如void print(const Fraction &f);之类的函数主体中调用

当前类的实现是不可变的,没有任何成员函数以任何方式更改内容。如果需要这样做,请执行numeratordenominator const

为了强制执行不变性,请在每个方法调用上创建一个新对象。不幸的是,对象创建不是免费的。取而代之的是,我让调用者决定是要新对象还是修改现有对象。

当前,分数的简化是由构造函数处理的。如果对象是不可变的,这很好。但是,我将创建一个simplify()方法来执行此简化。这使得逻辑可以在多个地方重用。然后,构造函数可能成为

Fraction(int n, int d) : numerator(n), denominator(d) { simplify(); }


正如其他人提到的,c ++的工作方式是重载运算符,而不是编写诸如add()之类的函数。将运算符+=-=*=/=定义为成员函数,然后根据这些定义非成员+-*/

Fraction& operator+=(const Fraction &f)
{
    numerator += f.numerator * denominator;
    denominator += f.denominator * nominator;
    simplify();
    return *this;
}

Fraction operator+(Fraction l, const Fraction &r)
{
    l += r;
    return l;
}


l是复制构造的,因为它按值传递,而传递的对象保持不变。该功能不会更改r,因此它也是const