C++-Programmierung/ Brüche/ Die Rechenoperationen


Addition, Subtraktion, Multiplikation und Division, das sind 4 Rechenoperationen. C++ bietet aber die mit der Zuweisung kombinierten Kurzschreibweisen, womit wir insgesamt auf 8 kommen.

Addition

Bearbeiten

Um zwei Brüche zu addieren, müssen die Nenner gleich sein. Wenn wir bereits Brüche haben, die sich nicht weiter kürzen lassen, (und dafür sorgt unsere Klasse,) dann erhalten wir wiederum einen unkürzbaren Bruch, wenn wir mit dem kgV erweitern. Das Ganze sieht also so aus:

Pseudocode


ErgebnisNenner  = kgV(Bruch1Nenner, Bruch2Nenner);
ErgebnisZaehler = Bruch1Zaehler * (ErgebnisNenner / Bruch1Nenner) +
    Bruch2Zaehler * (ErgebnisNenner / Bruch2Nenner);

Subtraktion

Bearbeiten

Für die Subtraktion gelten die gleichen Regeln wie bei der Addition.

Pseudocode


ErgebnisNenner  = kgV(Bruch1Nenner, Bruch2Nenner);
ErgebnisZaehler = Bruch1Zaehler * (ErgebnisNenner / Bruch1Nenner) -
    Bruch2Zaehler * (ErgebnisNenner / Bruch2Nenner);

Multiplikation

Bearbeiten

Bei der Multiplikation werden einfach die Zähler und die Nenner multipliziert. Danach muss der Bruch wieder gekürzt werden.

Pseudocode


ErgebnisZaehler = Bruch1Zaehler * Bruch2Zaehler;
ErgebnisNenner  = Bruch1Nenner  * Bruch2Nenner;
kuerzen();

Division

Bearbeiten

Die Division stimmt mit der Multiplikation fast überein, aber statt die Zähler und die Nenner miteinander zu multiplizieren, werden sie gegeneinander multipliziert.

Pseudocode


ErgebnisZaehler = Bruch1Zaehler * Bruch2Nenner;
ErgebnisNenner  = Bruch1Nenner  * Bruch2Zaehler;
kuerzen();

Kombination

Bearbeiten

Da C++ wie schon gesagt neben den normalen Rechenoperatoren noch die mit der Zuweisung kombinierten zur Verfügung stellt, werden wir einen kleinen Trick anwenden, um uns doppelte Arbeit zu ersparen. Wir werden die eigentlichen Rechenoperationen in den Zuweisungskombioperatoren implementieren und dann innerhalb der normalen Rechenoperatoren temporäre Objekte anlegen, für welche wir die Kombinationsoperatoren aufrufen. Das ist ein übliches und viel angewandtes Verfahren, welches einige Vorteile zu bieten hat. Sie sparen sich die doppelte Schreibarbeit und müssen sich bei Veränderungen nur um die Kombioperatoren kümmern, da sich die anderen ja genauso verhalten.

Die umgekehrte Variante, also von den Kombioperatoren die normalen aufrufen zu lassen, ist übrigens nicht zu empfehlen, da die Kombinationsoperatoren immer schneller sind, sie benötigen schließlich keine temporären Objekte. Außerdem ist es in vielen Klassen nötig, die normalen Rechenoperatoren außerhalb der Klasse zu deklarieren. Wenn sie nicht als friend deklariert sind, haben sie keinen Zugriff auf die privaten Member der Klasse, rufen Sie dagegen die Kombioperatoren auf, brauchen Sie gar keinen Zugriff.

Die normalen Rechenoperatoren werden als Nicht-Member implementiert. Dies ist nötig, damit für beide Parameter eine implizite Typumwandlung erfolgen kann. Andernfalls könnten Sie zwar Bruch(1, 2)+3 schreiben, aber nicht 3+Bruch(1, 2) und das ist natürlich nicht gewollt. Die Deklaration als Nicht-Member hat zur Folge, das wir zwei Parameter übergeben müssen. Der erste wird als Kopie („Call by value“) übergeben, da wir ja sowieso eine temporäre Kopie benötigen. Den Zweiten übergeben wir wie üblich als Referenz auf const. Da die Implementierung so kurz ist, sind diese Funktionen natürlich inline. Der Rückgabetyp lautet immer Bruch const. Dies verhindert, dass Sie so etwas wie a + b = 4 schreiben können, obwohl doch a + b == 4 gemeint war.

Abschluss

Bearbeiten

So, nun haben Sie wirklich genug Theorie gehört, es wird Zeit zu zeigen, wie das Ganze im Quelltext aussieht. Der Code lässt sich zwar noch nicht ausführen, weil der Konstruktor noch nicht definiert ist, aber es lohnt sich trotzdem, schon mal einen Blick darauf zu werfen.

unsigned int ggT(unsigned int a, unsigned int b){
    if(b == 0)                 // Wenn b gleich 0
        return a;              // ggT gefunden
    else return ggT(b, a % b); // andernfalls weitersuchen
}

unsigned int kgV(unsigned int a, unsigned int b){
    // Das kgV zweier Zahlen, ist ihr Produkt geteilt durch ihren ggT
    return a * b / ggT(a, b);
}

class Bruch{
public:
    Bruch(int zaehler = 0, unsigned int nenner = 1); // noch nicht definiert

    int          zaehler()const { return zaehler_; }  // Gibt Zähler zurück
    unsigned int nenner()const  { return nenner_; }   // Gibt Nenner zurück

    Bruch& operator+=(Bruch const& lvalue);
    Bruch& operator-=(Bruch const& lvalue);
    Bruch& operator*=(Bruch const& lvalue);
    Bruch& operator/=(Bruch const& lvalue);

private:
    void kuerzen();                                  // kürzt weitestmöglich

    int          zaehler_;
    unsigned int nenner_;
};


// Diese Methoden erstellen eine temporäre Kopie (lhs) ihres Objekts, führen
// die Rechenoperation auf ihr aus und geben sie dann zurück
inline Bruch const operator+(Bruch lhs, Bruch const &rhs){ return lhs += rhs; }
inline Bruch const operator-(Bruch lhs, Bruch const &rhs){ return lhs -= rhs; }
inline Bruch const operator*(Bruch lhs, Bruch const &rhs){ return lhs *= rhs; }
inline Bruch const operator/(Bruch lhs, Bruch const &rhs){ return lhs /= rhs; }

void Bruch::kuerzen(){
    const unsigned int tmp = ggT(zaehler_, nenner_);     // ggT in tmp speichern
    zaehler_ /= tmp; // Zähler durch ggT teilen
    nenner_  /= tmp; // Nenner durch ggT teilen
}

Bruch& Bruch::operator+=(Bruch const &lvalue){
    const unsigned int tmp = kgV(nenner_, lvalue.nenner_);
    zaehler_ = zaehler_ * (tmp / nenner_) + lvalue.zaehler_ * (tmp / lvalue.nenner_);
    nenner_  = tmp;
    return *this; // Referenz auf sich selbst zurückgeben
}

Bruch& Bruch::operator-=(Bruch const &lvalue){
    const unsigned int tmp = kgV(nenner_, lvalue.nenner_);
    zaehler_ = zaehler_ * (tmp / nenner_) - lvalue.zaehler_ * (tmp / lvalue.nenner_);
    nenner_  = tmp;
    return *this; // Referenz auf sich selbst zurückgeben
}

Bruch& Bruch::operator*=(Bruch const &lvalue){
    zaehler_ *= lvalue.zaehler_;
    nenner_  *= lvalue.nenner_;
    kuerzen();    // Bruch wieder kürzen
    return *this; // Referenz auf sich selbst zurückgeben
}

Bruch& Bruch::operator/=(Bruch const &lvalue){
    zaehler_ *= lvalue.nenner_;
    nenner_  *= lvalue.zaehler_;
    kuerzen();    // Bruch wieder kürzen
    return *this; // Referenz auf sich selbst zurückgeben
}