C++-Programmierung/ Brüche
Aus Wikibooks
Zielgruppe:
Alle die üben wollen!
Lernziel:
Die ersten Erkenntnisse der objektorientierten Programmierung zu festigen.
Inhaltsverzeichnis |
Rechnen mit Brüchen
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 das Sie wissen was Brüche sind, sollte das kein all zu 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.
[Bearbeiten] Was diese Klasse bieten soll
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.
[Bearbeiten] Was diese Klasse nicht kann
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. Ein Umwandlung von Gleitkommazahlen in gebrochene Zahlen ist ebenfalls nicht vorgesehen.
[Bearbeiten] Ein erster Blick
Gleich werden Sie zum ersten mal einen Blick auf die Bruch-Klasse werfen, also machen Sie sich bereit:
public:
Bruch(int zaehler = 0, unsigned int nenner = 1);
private:
int m_zaehler;
unsigned int m_nenner;
};
Das war nicht besonders aufregend, die Klasse hat bisher nur einen Konstruktor und die Variablen m_zaehler und m_nenner. Beachten Sie aber das m_zaehler vom Datentyp int (Vorzeichenbehaftet) ist, während m_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
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 dem Vorüberlegungen dafür entschieden das Erweitern wegzulassen, beim der Addition und Subtraktion brauchen wir es aber oder genauer gesagt wir brauchen das damit in Zusammenhang stehende „kleinste gemeinsame Vielfache“. Im folgenden werden „kleinste gemeinsame Vielfache“ und „größter gemeinsamer Teiler“ mit kgV und ggT abgekürzt.
Wir haben also die folgenden Methoden:
- ggT
- kgV
- kuerzen
Dazu kommen noch 2 Zugriffsfunktionen, da sich der Benutzer unserer Klasse vielleicht für Zähler und Nenner des Bruchs interessiert:
- zaehler
- nenner
2 dieser 5 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.
[Bearbeiten] Zugriff
Die beiden Zugriffsmethoden tun nichts weiter als Zähler und Nenner des Bruchs zurückzugeben, daher schreiben wir Sie direkt in die Klassendeklaration.
public:
Bruch(int zaehler = 0, unsigned int nenner = 1);
int zaehler()const {return m_zaehler;}
unsigned int nenner()const {return m_nenner;}
private:
int m_zaehler;
unsigned int m_nenner;
};
Wie Sie sehen, haben die beiden Methoden den gleiche 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.
[Bearbeiten] ggT()
In der Schule haben Sie sicher gelernt, dass sich der ggT (größter gemeinsamer Teiler) durch Primfaktorzerlegung ermitteln lässt. Dieses Verfahren ist allerdings denkbar ungünstig, um es auf einem Rechner umzusetzen. Sie müssten zunächst mal die Primzahlen berechnen und das dauert, zumal Sie ja gar nicht wissen wie viele Primzahlen Sie überhaupt benötigen.
Deshalb bedingen wir uns eines anderen Verfahrens, dem 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, das Sie den Steinschen Algorithmus einsetzen.
Der euklidische Algorithmus beruht auf zwei Eigenschaften des größten gemeinsamen Teilers:
und
Wenn Sie eine genaue Beschreibung wünschen, dann schauen Sie sich doch mal den entsprechenden Wikipediaartikel an.
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 „Schleifen mal anders – Rekursion“ im Abschnitt „Weitere Grundelemente“.
[Bearbeiten] kgV()
Das kgV lässt sich ganz einfach Berechnen wenn Sie das ggT kennen. Daher schreiben wir die Funktion mit Hilfe der eben erstellten ggT-Funktion.
Damit kgV() auf ggT zugreifen kann, muss ggT() natürlich bekannt sein. Sie müssen die Deklaration (in diesem Fall den Prototyp von ggT() immer geschrieben haben, bevor Sie das entsprechende Element verwenden.
[Bearbeiten] kuerzen()
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.
public:
Bruch(int zaehler = 0, unsigned int nenner = 1);
int zaehler()const {return m_zaehler;}
unsigned int nenner()const {return m_nenner;}
private:
void kuerzen();
int m_zaehler;
unsigned int m_nenner;
};
void Bruch::kuerzen(){
unsigned int tmp(ggT(m_zaehler, m_nenner));
m_zaehler /= tmp;
m_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
Addition, Subtraktion, Multiplikation und Division, das sind 4 Rechenoperationen. C++ bietet aber die mit der Zuweisung kombinierten Kurzschreibweisen, womit wir insgesamt auf 8 kommen.
[Bearbeiten] Addition
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:
ErgebnisZähler = Bruch1Zähler * (ErgebnisNenner / Bruch2Nenner) +
Bruch2Zähler * (ErgebnisNenner / Bruch1Nenner);
[Bearbeiten] Subtraktion
Für die Subtraktion gelten die gleichen Regeln wie bei der Addition.
ErgebnisZähler = Bruch1Zähler * (ErgebnisNenner / Bruch2Nenner) -
Bruch2Zähler * (ErgebnisNenner / Bruch1Nenner);
[Bearbeiten] Multiplikation
Bei der Multiplikation werden einfach die Zähler und die Nenner multipliziert. Danach muss der Bruch wieder gekürzt werden.
ErgebnisNenner = Bruch1Nenner * Bruch2Nenner;
kuerzen()
[Bearbeiten] Division
Die Division stimmt mit der Multiplikation fast überein, aber statt die Zähler und die Nenner miteinander zu multiplizieren, werden Sie gegeneinander multipliziert.
ErgebnisNenner = Bruch1Nenner * Bruch2Zähler;
kuerzen()
[Bearbeiten] Kombination
Da C++ wie schon gesagt neben den normalen Rechenoperatoren noch die mit der Zuweisung kombinierten zu 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 vielangewantes Verfahren, welches einige Vorteile zu bieten hat. Sie sparen 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.
[Bearbeiten] Abschluss
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.
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 geteielt 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 m_zaehler;} // Gibt Zähler zurück
unsigned int nenner()const {return m_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);
// Diese Methoden erstellen eine Temporäre Kopie ihres Objekts, führen
// die Rechenoperation auf ihr aus und geben sie dann zurück
Bruch operator+(Bruch const &lvalue)const{return Bruch(*this)+=lvalue;}
Bruch operator-(Bruch const &lvalue)const{return Bruch(*this)-=lvalue;}
Bruch operator*(Bruch const &lvalue)const{return Bruch(*this)*=lvalue;}
Bruch operator/(Bruch const &lvalue)const{return Bruch(*this)/=lvalue;}
private:
void kuerzen(); // kürzt weitestmöglich
int m_zaehler;
unsigned int m_nenner;
};
void Bruch::kuerzen(){
const unsigned int tmp = ggT(m_zaehler, m_nenner); // ggT in tmp speichern
m_zaehler /= tmp; // Zähler durch ggT teilen
m_nenner /= tmp; // Nenner durch ggT teilen
}
Bruch& Bruch::operator+=(Bruch const &lvalue){
const unsigned int tmp = kgV(m_nenner, lvalue.m_nenner);
m_zaehler = m_zaehler * (tmp / m_nenner) + lvalue.m_zaehler * (tmp / lvalue.m_nenner);
m_nenner = tmp;
return *this; // Referenz auf sich selbst zurückgeben
}
Bruch& Bruch::operator-=(Bruch const &lvalue){
const unsigned int tmp = kgV(m_nenner, lvalue.m_nenner);
m_zaehler = m_zaehler * (tmp / m_nenner) - lvalue.m_zaehler * (tmp / lvalue.m_nenner);
m_nenner = tmp;
return *this; // Referenz auf sich selbst zurückgeben
}
Bruch& Bruch::operator*=(Bruch const &lvalue){
m_zaehler *= lvalue.m_zaehler;
m_nenner *= lvalue.m_nenner;
kuerzen(); // Bruch wieder kürzen
return *this; // Referenz auf sich selbst zurückgeben
}
Bruch& Bruch::operator/=(Bruch const &lvalue){
m_zaehler *= lvalue.m_nenner;
m_nenner *= lvalue.m_zaehler;
kuerzen(); // Bruch wieder kürzen
return *this; // Referenz auf sich selbst zurückgeben
}
Umwandlung aus anderen Datentypen
Um aus einer Variable eines andren 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:
public:
Bruch(int zaehler = 0, unsigned int nenner = 1);
// ...
};
Bruch::Bruch(int zaehler, unsigned int nenner):
m_zaehler(zaehler),
m_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 Buch 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 bruch2(5) // erzeugt (5/1)
Bruch bruch3(3, 4) // erzeugt (3/4)
Die erste Form entspricht einem Defaultkonstruktor und setzt den Wert des Buches auf (0/1), was dem ganzzahligen Wert entspricht. In der zweiten Form wird 1 int-Wert übergeben, der resultierende Buch 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.
Bitte beachten Sie, dass der Nenner eines Buches 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 Buch eine 0 als Nenner zu übergeben, erst einmal ignorieren. Später kommen wir auf diesen Punkt aber noch einmal zurück.
[Bearbeiten] Gleitkommazahl wird Bruch
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:
public:
Bruch(int zaehler = 0, unsigned int nenner = 1);
Bruch(double wert);
// ...
};
Bruch::Bruch(double wert):
m_zaehler(static_cast<int>(wert*1000000.0+0.5)),
m_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äher ( 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 3 Vorkomma- und 6 Nachkommastellen.
Wir setzen den Nenner des Buches auf 1000000, dann multiplizieren wir den übergebenen Wert mit 1000000. 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, das die Zahl nach den abschneiden der Nachkommastellen kaufmännisch gerundet wurde.
Alles was über 6 Nachkommastelle hinausgeht, ist also für uns nicht relevant, da es korrekt gerundet wird. Das Problem besteht darin, dass der Bruch keinen Zahler 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, daher werden wir damit leben, das dies nur ein Beispiel für die Umwandlung einer Gleitkommazahl in einen Bruch ist und keine perfekte Implementierung.
Anschließend wir im Funktionsrumpf wieder einmal die Funktion kuerzen() aufgerufen. Sie können nun also auch schreiben:
Bruch bruch2(7.0) // erzeugt (7/1)
Bruch bruch3(999.999999) // erzeugt (999999999/1000000)
[Bearbeiten] (k)ein Kopierkonstruktor
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:
Ein- und Ausgabe
Als nächstes wollen wir dafür sorgen, das 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. Includedateien gehören natürlich an die übliche Stelle am Dateianfang.
[Bearbeiten] Ausgabe
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 „ios“ includieren müssen, damit Sie die Standardstreams überladen können.
std::ostream& operator<<(std::ostream &os, Bruch const &bruch){
return os << '(' << bruch.zaehler() << '/' << bruch.nenner() << ')';
}
[Bearbeiten] Eingabe
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 setzten.
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);
return is;
}
}
}
is.setstate(std::ios_base::failbit);
return is;
}
Wie Sie sehen können, wird diesmal eine nichtkonstante 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 schiefgeht. Falls etwas daneben geht, wird das failbit gesetzt und anschließend der Stream zurückgegeben.
Umwandlung in andere Datentypen
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 einer Ganzzahldivision würden natürlich die Nachkommastellen abgeschnitten.
public:
// ...
operator float() {return static_cast<float>(m_zaehler) / m_nenner;}
operator double() {return static_cast<double>(m_zaehler) / m_nenner;}
operator long double(){return static_cast<long double>(m_zaehler) / m_nenner;}
// ...
};
Sie können den Bruch jetzt auf die folgende Weise in eine Gleitkommazahl umwandeln:
Der Taschenrechner geht zur Schule
Im Abschnitt „Einführung in C++“ gab es ein Kapitel über einen einfachen Taschenrechner. In diesem Kapitel werden wir ihn ganz leicht modifizieren, so das er mit Brüchen statt mit Gleitkommazahlen rechnet und die Ausgabe als Brüche und als Gleitkommazahlen gemacht wird.
// 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';
}
(1/4) * (1/2) = (1/8)
0.25 * 0.5 = 0.125
In der Zusammenfassung finden Sie noch einmal das gesamte Programm.
Zusammenfassung
Hier finden Sie noch einmal das komplette Programm, inklusive der Ausgabe eines Durchlaufs.
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 m_zaehler;} // Gibt Zähler zurück
unsigned int nenner()const {return m_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);
// Diese Methoden erstellen eine Temporäre Kopie ihres Objekts, führen
// die Rechenoperation auf ihr aus und geben sie dann zurück
Bruch operator+(Bruch const &lvalue)const{return Bruch(*this)+=lvalue;}
Bruch operator-(Bruch const &lvalue)const{return Bruch(*this)-=lvalue;}
Bruch operator*(Bruch const &lvalue)const{return Bruch(*this)*=lvalue;}
Bruch operator/(Bruch const &lvalue)const{return Bruch(*this)/=lvalue;}
// Umwandlung in Gleitkommatypen
operator float() {return static_cast<float>(m_zaehler)/m_nenner;}
operator double() {return static_cast<double>(m_zaehler)/m_nenner;}
operator long double(){return static_cast<long double>(m_zaehler)/m_nenner;}
private:
void kuerzen(); // kürzt weitestmöglich
int m_zaehler;
unsigned int m_nenner;
};
Bruch::Bruch(int zaehler, unsigned int nenner):
m_zaehler(zaehler),
m_nenner(nenner){
kuerzen();
}
Bruch::Bruch(double wert):
m_zaehler(static_cast<int>(wert*1000000.0+0.5)),
m_nenner(1000000){
kuerzen();
}
void Bruch::kuerzen(){
unsigned int tmp = ggT(m_zaehler, m_nenner); // ggT in tmp speichern
m_zaehler /= tmp; // Zähler durch ggT teilen
m_nenner /= tmp; // Nenner durch ggT teilen
}
Bruch& Bruch::operator+=(Bruch const &lvalue){
unsigned int tmp = kgV(m_nenner, lvalue.m_nenner);
m_zaehler = m_zaehler * (tmp / m_nenner) + lvalue.m_zaehler * (tmp / lvalue.m_nenner);
m_nenner = tmp;
return *this; // Referenz auf sich selbst zurückgeben
}
Bruch& Bruch::operator-=(Bruch const &lvalue){
unsigned int tmp = kgV(m_nenner, lvalue.m_nenner);
m_zaehler = m_zaehler * (tmp / m_nenner) - lvalue.m_zaehler * (tmp / lvalue.m_nenner);
m_nenner = tmp;
return *this; // Referenz auf sich selbst zurückgeben
}
Bruch& Bruch::operator*=(Bruch const &lvalue){
m_zaehler *= lvalue.m_zaehler;
m_nenner *= lvalue.m_nenner;
kuerzen(); // Bruch wieder kürzen
return *this; // Referenz auf sich selbst zurückgeben
}
Bruch& Bruch::operator/=(Bruch const &lvalue){
m_zaehler *= lvalue.m_nenner;
m_nenner *= lvalue.m_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';
}
(1/5) - (3/4) = (-11/20)
0.2 - 0.75 = -0.55

