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
}