Brüche

Zielgruppe:

Alle die üben wollen!

Lernziel:
Die ersten Erkenntnisse der objektorientierten Programmierung zu festigen.


Rechnen mit Brüchen [Bearbeiten]

Das Rechnen mit Ganzzahlen und Gleitkommazahlen haben Sie inzwischen gelernt. In diesem Kapitel kehren wir das Prinzip mal um. Jetzt werden Sie Ihrem Rechner beibringen mit Brüchen zu rechnen! Davon ausgehend, dass Sie wissen was Brüche sind, sollte das kein allzu großes Problem darstellen. Die Klasse, die am Ende dieses Abschnittes entstanden ist, wird noch nicht perfekt sein, aber sie wird zum normalen Rechnen ausreichen. Sie dürfen sie natürlich jederzeit weiterentwickeln und verbessern.

Was diese Klasse bieten soll

Bearbeiten

Bevor wir nun Hals über Kopf anfangen, Quellcode zu schreiben, sollten wir uns erst klar machen, was genau wir überhaupt erreichen wollen.

Brüche bestehen aus einem Zähler und einem Nenner. Diese sind Ganzzahlen, allerdings ist der Nenner eine natürliche Zahl (ohne Vorzeichen) und der Zähler eine ganze Zahl (mit Vorzeichen). Daher werden wir den entsprechenden Variablen passende Datentypen zuordnen.

Da es beim Rechnen leichter ist, die Brüche gleich in ihrer kleinsten (nicht mehr zu kürzenden) Form zu sehen, werden wir den Bruch nach jeder Rechenoperation kürzen. Weiterhin wollen wir mit den Brüchen rechnen können wie mit den eingebauten Datentypen für Zahlen. Natürlich brauchen wir eine Ein- und Ausgabe für unsere Brüche. Schließlich und endlich wollen wir sie beim Rechnen mit den eingebauten Datentypen mischen.

Was diese Klasse nicht kann

Bearbeiten

Die logische Folge des automatischen Kürzens ist natürlich, dass sich die Brüche nicht mit beliebigen Zahlen erweitern und kürzen lassen. Es ist natürlich möglich, beides gleichzeitig zu realisieren, aber es ist nicht sinnvoll, also machen wir das auch nicht. Eine Umwandlung von Gleitkommazahlen in gebrochene Zahlen ist ebenfalls nicht vorgesehen.

Ein erster Blick

Bearbeiten

Gleich werden Sie zum ersten Mal einen Blick auf die Bruch-Klasse werfen, also machen Sie sich bereit:

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

private:
    int          zaehler_;
    unsigned int nenner_;
};

Das war nicht besonders aufregend, die Klasse hat bisher nur einen Konstruktor und die Variablen zaehler_ und nenner_. Beachten Sie aber, dass zaehler_ vom Datentyp int (vorzeichenbehaftet) ist, während nenner_ den Datentyp unsigned int (vorzeichenlos) hat.

Auf die Standardparameter des Konstruktors werden wir in einem späteren Kapitel noch einmal zu sprechen kommen. Für den Moment soll es reichen, seine Deklaration zu zeigen.

Die Methoden [Bearbeiten]

In diesem Kapitel werden Sie mit den Methoden bekannt gemacht, die unsere Bruch-Klasse zur Verfügung stellt. Grundlegend tauchen im Zusammenhang mit Brüchen zwei Begriffe auf: „Erweitern“ und „Kürzen“. Im Zusammenhang mit diesen beiden finden sich wiederum die Begriffe „kleinstes gemeinsames Vielfaches“ und „größter gemeinsamer Teiler“. Wir haben uns bei den Vorüberlegungen dafür entschieden, das Erweitern wegzulassen, bei der Addition und Subtraktion brauchen wir es aber. Im folgenden werden das „kleinste gemeinsame Vielfache“ und der „größte gemeinsame Teiler“ mit kgV und ggT abgekürzt.

Wir haben also die folgenden Methoden:

  • ggT
  • kgV
  • kuerzen

Dazu kommen noch zwei Zugriffsfunktionen, da sich der Benutzer unserer Klasse vielleicht für Zähler und Nenner des Bruchs interessiert:

  • zaehler
  • nenner

Zwei dieser fünf Methoden müssen aber gleich wieder gestrichen werden. ggT und kgV sind zwar in Zusammenhang mit Brüchen hilfreich, aber sie beschreiben allgemeine mathematische Funktionen, die nicht ausschließlich in Zusammenhang mit Brüchen eingesetzt werden. Daher sind diese beiden Funktionen unter der Überschrift Methoden etwas fehlpositioniert, da sie keine Member der Klasse Bruch sind.

Die beiden Zugriffsmethoden tun nichts weiter, als Zähler und Nenner des Bruchs zurückzugeben, daher schreiben wir sie direkt in die Klassendeklaration.

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

    int          zaehler()const {return zaehler_;}
    unsigned int nenner()const  {return nenner_;}

private:
    int          zaehler_;
    unsigned int nenner_;
};

Wie Sie sehen, haben die beiden Methoden den gleichen Rückgabetyp wie die Variablen, die sie repräsentieren. Achten Sie bitte auch darauf, dass beide als const deklariert sind, da sie keine Variable innerhalb der Klasse verändern.

In der Schule haben Sie sicher gelernt, dass sich der ggT (größte gemeinsame Teiler) durch Primfaktorzerlegung ermitteln lässt. Dieses Verfahren ist allerdings denkbar ungünstig, um es auf einem Rechner umzusetzen. Sie müssten zunächst einmal die Primzahlen berechnen und das dauert, zumal Sie ja gar nicht wissen, wie viele Primzahlen Sie überhaupt benötigen.

Deshalb bedienen wir uns eines anderen Verfahrens, des euklidischen Algorithmus. Eine Verbesserung dieses Verfahrens ist der steinsche Algorithmus, aber für unsere Zwecke reicht ersterer. Sie dürfen die Klasse natürlich gerne dahingehend verbessern, dass Sie den steinschen Algorithmus einsetzen.

Der euklidische Algorithmus beruht auf zwei Eigenschaften des größten gemeinsamen Teilers:

  1.  
  2.  

Wenn Sie eine genaue Beschreibung wünschen, dann schauen Sie sich doch mal den entsprechenden Wikipediaartikel an.

unsigned int ggT(unsigned int a, unsigned int b){
    if(b == 0)
        return a;
    else return ggT(b, a % b);
}

Beachten Sie, dass diese Funktion rekursiv funktioniert. Die Implementierung als einfache Schleife wäre ebenfalls möglich gewesen, aber sie ist etwas unübersichtlicher und dank Optimierung ist die rekursive Variante nur unwesentlich langsamer. Wenn Sie nicht mehr wissen, was rekursive Funktionen sind, dann werfen Sie noch mal einen Blick auf das Kapitel Rekursion im Abschnitt „Weitere Grundelemente“.

Das kgV lässt sich ganz einfach berechnen, wenn Sie den ggT kennen. Daher schreiben wir die Funktion mit Hilfe der eben erstellten ggT-Funktion.

unsigned int kgV(unsigned int a, unsigned int b){
    return (a * b) / ggT(a, b);
}
Hinweis

Damit kgV() auf ggT() zugreifen kann, muss dieses natürlich bekannt sein. Sie müssen die Deklaration immer geschrieben haben, bevor Sie das entsprechende Element verwenden.

kuerzen()

Bearbeiten

kuerzen() ist nun endlich mal wirklich eine Memberfunktion (oder auch Methode) von Bruch. Da sie somit direkten Zugriff auf die Variablen hat, die sie verändern soll, braucht sie keine Argumente und auch keinen Rückgabewert.

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

    int          zaehler()const {return zaehler_;}
    unsigned int nenner()const  {return nenner_;}

private:
    void kuerzen();

    int          zaehler_;
    unsigned int nenner_;
};

void Bruch::kuerzen(){
    unsigned int tmp(ggT(zaehler_, nenner_));
    zaehler_ /= tmp;
    nenner_  /= tmp;
}

Da kuerzen() eine Methode ist, die nur innerhalb der Klasse nach einer Rechenoperation aufgerufen wird, deklarieren wir sie im privaten Bereich der Klasse.

Die Rechenoperationen [Bearbeiten]

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
}

Umwandlung aus anderen Datentypen [Bearbeiten]

Um aus einer Variable eines anderen Datentyps in einen Bruch umzuwandeln, übergibt man einem Konstruktor diese Variable und lässt ihn die Umwandlung durchführen. Sie erinnern sich bestimmt noch daran, dass im ersten Kapitel dieses Abschnittes stand, die Standardparameter des Konstruktors würden später besprochen. Dieser Zeitpunkt ist nun gekommen. Zur Erinnerung, seine Deklaration innerhalb der Klasse lautete:

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

// ...
};

Bruch::Bruch(int zaehler, unsigned int nenner):
    zaehler_(zaehler),
    nenner_(nenner){
    kuerzen();
}

Die Definition steht, wie Sie oben sehen, außerhalb der Klasse. Innerhalb der Initialisierungsliste, welche durch einen Doppelpunkt eingeleitet wird, werden die Membervariablen mit den übergebenen Parametern initialisiert. Innerhalb des Funktionsrumpfes wird die Methode kuerzen() aufgerufen, um den Bruch falls nötig zu kürzen.

Beachten Sie, dass die Standardparameter nur bei der Deklaration, nicht aber bei der Definition einer Funktion angegeben werden. Unser Konstruktor übernimmt 2 int-Werte und beide besitzen einen Standardwert, daher kann er in 3 Formen aufgerufen werden:

Bruch bruch1       // erzeugt (0/1)
Bruch bruch2(5)    // erzeugt (5/1)
Bruch bruch3(3, 4) // erzeugt (3/4)

Die erste Form entspricht einem Defaultkonstruktor und setzt den Wert des Bruches auf (0/1), was dem ganzzahligen Wert 0 entspricht. In der zweiten Form wird 1 int-Wert übergeben, der resultierende Bruch entspricht diesem Wert, da der Nenner dank des Standardparameters auf 1 gesetzt wird. Die dritte Form übernimmt schließlich 2 int-Werte, der erste gibt den Zähler und der zweite den Nenner des Bruches an.

Auch an dieser Stelle soll noch einmal darauf hingewiesen werden, dass der Nenner nicht negativ sein kann, daher ist dieser Parameter auch vom Typ unsigned int welcher nur positive Werte zulässt.

Thema wird später näher erläutert…

Bitte beachten Sie, dass der Nenner eines Bruches normalerweise nicht 0 sein kann. Normalerweise würde man dem in C++ mit einer Ausnahme begegnen. Da wir Ausnahmen aber bisher nicht besprochen haben und es auch noch eine Weile dauern wird, bis wir dieses Thema behandeln, werden wir dem Umstand, dass es möglich ist dem Konstruktor von Bruch eine 0 als Nenner zu übergeben, erst einmal ignorieren. Später kommen wir auf diesen Punkt aber noch einmal zurück.

Gleitkommazahl wird Bruch

Bearbeiten

Wir haben jetzt die Umwandlung von ganzen Zahlen in Brüche besprochen. Als nächstes wollen wir eine Gleitkommazahl in einen Bruch umwandeln. Hierfür benötigen wir einen zweiten Konstruktor, welcher eine Gleitkommazahl übernimmt und sie in einen Bruch umwandelt:

class Bruch{
public:
    Bruch(int zaehler = 0, unsigned int nenner = 1);
    Bruch(double wert);

// ...
};

Bruch::Bruch(double wert):
    zaehler_(static_cast<int>(wert*1000000.0+0.5)),
    nenner_(1000000){
    kuerzen();
}

Es ist kaum möglich, einen double-Wert zuverlässig in einen Bruch umzuwandeln, da die Datentypen, die wir für Zähler (int) und Nenner (unsigned int) verwenden, den Wertebereich unserer Brüche gewaltig einschränken. Dieser Konstruktor ist daher kaum praxistauglich, aber als Beispiel sollte er genügen. Einmal vorausgesetzt, dass ein int 4 Byte groß ist, beachtet er drei Vorkomma- und sechs Nachkommastellen.

Wir setzen den Nenner des Bruches auf 1000000, dann multiplizieren wir den übergebenen Wert mit 1 000 000. Würde man jetzt Zähler durch Nenner teilen, hätte man wieder exakt den Wert, der übergeben wurde. Da der Zähler aber ein int- und kein double-Wert sein muss, müssen wir ihn noch umwandeln. Da eine solche Umwandlung aber alle Nachkommastellen abschneidet, anstatt kaufmännisch korrekt zu runden, addieren wir den double-Wert vorher mit 0.5, was dazu führt, dass die Zahl nach dem Abschneiden der Nachkommastellen kaufmännisch gerundet wurde.

Alles was über 6 Nachkommastellen hinausgeht ist also für uns nicht relevant, da es korrekt gerundet wird. Das Problem besteht darin, dass der Bruch keinen Zähler aufnehmen kann, der größer als der größtmögliche int-Wert (bei 4 Byte 2147483647) ist. Leider lässt sich dieses Problem nicht ohne weiteres lösen, sodass wir uns an dieser Stelle damit zufrieden geben, dass dies nur eine beispielhafte Implementierung für eine Umwandlung einer Gleitkommazahl in einen Bruch und keine perfekte Implementierung ist.

Anschließend wird im Funktionsrumpf wieder einmal die Funktion kuerzen() aufgerufen. Sie können nun also auch schreiben:

Bruch bruch1(0.25)       // erzeugt (1/4)
Bruch bruch2(7.0)        // erzeugt (7/1)
Bruch bruch3(999.999999) // erzeugt (999999999/1000000)

(K)ein Kopierkonstruktor

Bearbeiten

Für unsere Bruch-Klasse werden wir keinen Kopierkonstruktor anlegen, da unser Compiler uns diese Arbeit mit einem zufriedenstellenden Ergebnis abnimmt. Oder anders ausgedrückt, Sie können, ohne eine einzige Zeile Code zur Klasse hinzuzufügen, schreiben:

Bruch bruch1(1, 5);   // erzeugt (1/5)
Bruch bruch2(bruch1); // erzeugt (1/5)

Ein- und Ausgabe [Bearbeiten]

Als nächstes wollen wir dafür sorgen, dass unsere Brüche ebenso einfach ein- und ausgegeben werden können, wie die elementaren Datentypen. Sie wissen ja bereits, dass Sie hierfür nur den Ein- und Ausgabeoperator überladen müssen. Wir wollen die Brüche in der folgenden Form schreiben und lesen:

(Zähler/Nenner)

Die beiden folgenden Operatorfunktionen können Sie nach der Deklaration der Bruch-Klasse einfügen. Include-Dateien gehören natürlich an die übliche Stelle am Dateianfang.

Die Ausgabe lässt sich ziemlich simpel realisieren, wir geben einfach das von uns gewünschte Format auf dem Ausgabestream aus, den wir mittels Parameter erhalten. Dann geben wir den Stream zurück. Beachten Sie allerdings, dass Sie die Headerdatei „iostream“ inkludieren müssen, damit Sie die Standardstreams überladen können.

#include <iostream>

std::ostream& operator<<(std::ostream &os, Bruch const &bruch){
    return os << '(' << bruch.zaehler() << '/' << bruch.nenner() << ')';
}

Die Eingabe ist etwas schwieriger. Wir müssen sicherstellen, dass das von uns vorgegebene Format eingehalten wird und falls nötig, den Eingabestream auf einen Fehlerstatus zu setzen.

#include <iostream>

std::istream& operator>>(std::istream &is, Bruch &bruch){
    char tmp;
    int zaehler;
    unsigned int nenner;
    is >> tmp;
    if(tmp == '('){
        is >> zaehler;
        is >> tmp;
        if(tmp == '/'){
            is >> nenner;
            is >> tmp;
            if(tmp == ')'){
                bruch = Bruch(zaehler, nenner);
                return is;
            }
        }
    }
    is.setstate(std::ios_base::failbit);
    return is;
}

Wie Sie sehen können, wird diesmal eine nicht-konstante Referenz auf einen Bruch übergeben, da dieser ja geändert wird, wenn man ihm einen neuen Wert zuweist. Da die Klasse Bruch keine Möglichkeit bereitstellt, zaehler oder nenner einzeln zu ändern, erstellen wir nach dem Einlesen beider einen entsprechenden Bruch und weisen ihn an unseren zu. Auf diese Weise stellen wir auch sicher, dass sich an dem Bruch nichts verändert, wenn während des Einlesens irgendetwas schief geht. Falls etwas daneben geht, wird das failbit gesetzt und anschließend der Stream zurückgegeben.

Umwandlung in andere Datentypen [Bearbeiten]

In diesem Kapitel werden wir unseren Bruch in Gleitkommazahlen umwandeln. Hierfür müssen wir einfach den Zähler durch den Nenner teilen. Da jedoch beide einen integralen Typ haben, müssen wir vor dem Teilen einen der Werte in eine Gleitkommazahl umwandeln, damit keine Ganzzahldivision durchgeführt wird. Bei Ganzzahldivisionen würden natürlich die Nachkommastellen abgeschnitten.

class Bruch{
public:
// ...
    operator float()      {return static_cast<float>(zaehler_) / nenner_;}
    operator double()     {return static_cast<double>(zaehler_) / nenner_;}
    operator long double(){return static_cast<long double>(zaehler_) / nenner_;}
// ...
};

Sie können den Bruch jetzt auf die folgende Weise in eine Gleitkommazahl umwandeln:

#include <iostream>

// Alles was zur Bruch-Klasse gehört

int main(){
    std::cout << static_cast<double>(Bruch(1, 8));
}
Ausgabe:
0.125

Das Kapitel Casts enthält weitergehende Informationen zur Umwandlung von Datentypen.

Der Taschenrechner geht zur Schule [Bearbeiten]

Im Abschnitt „Einführung in C++“ gab es ein Kapitel über einen einfachen Taschenrechner. In diesem Kapitel werden wir ihn ganz leicht modifizieren, so dass er mit Brüchen statt mit Gleitkommazahlen rechnet und die Ausgabe als Brüche und als Gleitkommazahlen gemacht wird.

#include <iostream>

// Alles was zur Bruch-Klasse gehört

int main(){
    Bruch zahl1, zahl2, ergebnis;                      // Variablen für Zahlen vom Typ Bruch
    char rechenzeichen;                                // Variable fürs Rechenzeichen
 
    std::cout << "Geben Sie eine Rechenaufgabe ein: "; // Eingabeaufforderung ausgeben
    std::cin >> zahl1 >> rechenzeichen >> zahl2;       // Aufgabe einlesen
 
    switch(rechenzeichen){                             // Wert von rechenzeichen ermitteln
        case '+': ergebnis = zahl1+zahl2; break;       // entsprechend dem
        case '-': ergebnis = zahl1-zahl2; break;       // Rechenzeichen
        case '*': ergebnis = zahl1*zahl2; break;       // das Ergebnis
        case '/': ergebnis = zahl1/zahl2; break;       // berechnen
        // Fehlerausgabe und Programm beenden, falls falsches Rechenzeichen
        default: std::cout << "unbekanntes Rechenzeichen...\n"; return 1;
    }
 
    // Aufgabe noch mal komplett ausgeben
    std::cout << zahl1 << ' ' << rechenzeichen << ' ' << zahl2 << " = " << ergebnis << '\n';
    std::cout << static_cast<double>(zahl1) << ' '     // Ausgabe als
              << rechenzeichen << ' '                  // Gleitkommawerte
              << static_cast<double>(zahl2) << " = "
              << static_cast<double>(ergebnis) << '\n';
}
Ausgabe:
Geben Sie eine Rechenaufgabe ein: <Eingabe>(1/4)*(1/2)</Eingabe>
(1/4) * (1/2) = (1/8)
0.25 * 0.5 = 0.125

In der Zusammenfassung finden Sie noch einmal das gesamte Programm.

Zusammenfassung [Bearbeiten]

Hier finden Sie noch einmal das komplette Programm, inklusive der Ausgabe eines Durchlaufs.

#include <iostream>

unsigned int ggT(unsigned int a, unsigned int b){
    if(b == 0)
        return a;
    else return ggT(b, a % b);
}

unsigned int kgV(unsigned int a, unsigned int b){
    return a/ggT(a,b) * b;
}

class Bruch{
public:
    Bruch(int zaehler = 0, unsigned int nenner = 1); // Konstruktoren
    Bruch(double wert);                              // dieser ist nicht perfekt

    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);

    // Umwandlung in Gleitkommatypen
    operator float()      {return static_cast<float>(zaehler_)/nenner_;}
    operator double()     {return static_cast<double>(zaehler_)/nenner_;}
    operator long double(){return static_cast<long double>(zaehler_)/nenner_;}

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; }


Bruch::Bruch(int zaehler, unsigned int nenner):
    zaehler_(zaehler),
    nenner_(nenner){
    kuerzen();
}

Bruch::Bruch(double wert):
    zaehler_(static_cast<int>(wert*1000000.0+0.5)),
    nenner_(1000000){
    kuerzen();
}

void Bruch::kuerzen(){
    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){
    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){
    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
}

std::ostream& operator<<(std::ostream &os, Bruch const &bruch){
    return os << '(' << bruch.zaehler() << '/' << bruch.nenner() << ')';
}

std::istream& operator>>(std::istream &is, Bruch &bruch){
    char tmp;
    int zaehler, nenner;
    is >> tmp;
    if(tmp=='('){
        is >> zaehler;
        is >> tmp;
        if(tmp=='/'){
            is >> nenner;
            is >> tmp;
            if(tmp==')'){
                bruch=Bruch(zaehler, nenner); // Bruch erzeugen und Wert übernehmen
                return is;
            }
        }
    }
    is.setstate(std::ios_base::failbit);      // Fehlerstatus setzen
    return is;
}

int main(){
    Bruch zahl1, zahl2, ergebnis;                      // Variablen für Zahlen vom Typ Bruch
    char rechenzeichen;                                // Variable fürs Rechenzeichen

    std::cout << "Geben Sie eine Rechenaufgabe ein: "; // Eingabeaufforderung ausgeben
    std::cin >> zahl1 >> rechenzeichen >> zahl2;       // Aufgabe einlesen

    switch(rechenzeichen){                             // Wert von rechenzeichen ermitteln
        case '+': ergebnis = zahl1+zahl2; break;       // entsprechend dem
        case '-': ergebnis = zahl1-zahl2; break;       // Rechenzeichen
        case '*': ergebnis = zahl1*zahl2; break;       // das Ergebnis
        case '/': ergebnis = zahl1/zahl2; break;       // berechnen
        // Fehlerausgabe und Programm beenden, falls falsches Rechenzeichen
        default: std::cout << "unbekanntes Rechenzeichen...\n"; return 1;
    }

    // Aufgabe noch mal komplett ausgeben
    std::cout << zahl1 << ' ' << rechenzeichen << ' ' << zahl2 << " = " << ergebnis << '\n';
    std::cout << static_cast<double>(zahl1) << ' '     // Ausgabe als
              << rechenzeichen << ' '                  // Gleitkommawerte
              << static_cast<double>(zahl2) << " = "
              << static_cast<double>(ergebnis) << '\n';
}
Ausgabe:
Geben Sie eine Rechenaufgabe ein: <Eingabe>(1/5)-(3/4)</Eingabe>
(1/5) - (3/4) = (-11/20)
0.2 - 0.75 = -0.55