1. Einleitung
Wenn Sie zum ersten Mal mit einem double arbeiten, tippt die Hand oft automatisch if (a == b). Das ist normal: das Gehirn ist daran gewöhnt, dass „gleich“ auch wirklich „gleich“ bedeutet. Das Problem ist, dass für Fließkommazahlen == nicht das «mathematische Gleichsein» vergleicht, sondern die Übereinstimmung konkreter darstellbarer Werte im Speicher. Zahlen wie 0.1 werden in der Binärdarstellung oft als nächstes darstellbares Approximation gespeichert und nicht als das «ideale 0.1». Deshalb kann == in realen Berechnungen leicht zum Erzeuger zufälliger Überraschungen werden.
Das bekannteste Beispiel:
#include <iostream>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
std::cout << (a == b) << '\n'; // häufig 0
}
Warum «häufig»? Weil das genaue Verhalten von Details der Darstellung und der Ausgabe abhängt, aber die Grundidee bleibt: verlassen Sie sich nicht auf exakte Gleichheit von Ergebnissen mit Gleitkommazahlen.
Wichtig ist ein Detail: wenn Sie Werte vergleichen, die Sie selbst als gleiche Literale zugewiesen haben (z. B. double x = 1.5; double y = 1.5;), dann kann == durchaus angemessen sein. Probleme tauchen dort auf, wo Arithmetik, Schleifen, Formeln, Konversionen, Benutzereingaben usw. beteiligt sind — also fast überall, wo das Programm etwas Nützliches tut.
2. Epsilon-Vergleich: was bedeutet abs(a - b) < eps
Um mit Fehlern umzugehen, führen wir eine einfache «ingenieurmäßige» Idee ein: Zahlen gelten als gleich, wenn sie hinreichend nahe beieinander liegen. Diese «Hinreichendnähe» definieren wir durch die Zahl eps (Epsilon). An dieser Stelle seufzt die Mathematik ein wenig, aber die Ingenieurskunst freut sich: nun vergleichen wir nicht das Ideal, sondern eine Toleranz — wie in technischen Zeichnungen.
Die grundlegende Formel lautet:
\[ |a - b| < eps \]
Im Code sieht das so aus (vorerst ohne <cmath>, weil die nächste Vorlesung genau davon handeln wird):
#include <iostream>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
double diff = a - b;
if (diff < 0.0) diff = -diff; // manuelle abs
const double eps = 1e-12;
std::cout << (diff < eps) << '\n'; // normalerweise 1
}
Die Bedeutung von eps ist hier sehr wichtig: wir beweisen nicht «mathematische Gleichheit», sondern treffen eine Entscheidung «im Rahmen der Aufgabenpräzision ist das dasselbe». In Aufgaben zu Geld, Koordinaten, Prozenten, Physik oder Geometrie ist das fast immer genau das, was gebraucht wird.
Und ja, das ist der Moment, in dem ein Programmierer erwachsen wird: er hört auf zu fragen «warum nicht gleich?» und beginnt zu fragen «wie nah sind sie?».
3. Wie wählt man eps: Vereinbarung, kein „magische Zahl“
Beim Wählen von eps machen Anfänger meist zwei Extreme: entweder setzen sie 0.0000000000000000000001 (um ganz sicher zu sein!), oder sie nehmen 0.1 (weil „was macht das schon“). Tatsächlich ist eps Teil des Vertrags Ihrer Aufgabe. Er hängt davon ab, in welchen Einheiten Sie rechnen, welche Größenordnungen Ihre Zahlen haben und welche Genauigkeit überhaupt sinnvoll ist.
Stellen Sie sich vor, Sie messen Entfernungen zwischen Städten in Kilometern. Dort ist eps = 1e-12 sinnlos: keine Eingabe, keine Karte und kein GPS arbeitet so genau. Wenn Sie dagegen in der Physik mit sehr kleinen Größenordnungen rechnen, kann eps = 1e-3 schon zu grob sein.
Hilfreich ist diese grobe Orientierung (kein Gesetz, nur Anhaltspunkt für Einsteiger):
| Aufgabenkontext (ungefähr) | Sinnvolle Größenordnung von eps |
|---|---|
| „Schulmathematik“, einfache Berechnungen mit double | 1e-12 … 1e-9 |
| Koordinaten/Geometrie in „normalen“ Einheiten | 1e-9 … 1e-6 |
| Geld/Preise (falls Sie unbedingt mit double rechnen, was diskutabel ist) | 1e-6 … 1e-2 (abhängig von Währung/Abrundung) |
| Werte mit Rauschen (Messungen, Sensoren) | abhängig vom Rauschen der Daten |
Grundprinzip: eps muss zum Sinn der Aufgabe passen. Wenn Sie eine Zahl auf zwei Nachkommastellen ausgeben, dann wirkt eps = 1e-12 wie jemand, der die Temperatur eines Teekessels mit einem Mikroskop misst.
Später (auf fortgeschrittenerem Niveau) kommt das Konzept der relativen Fehlertoleranz dazu (wenn eps von der Größenordnung der Zahlen abhängt), aber heute ist das Ziel, grundlegende Stabilität zu lernen, ohne die Theorie zu überfrachten.
4. Funktionen für robuste Vergleiche
Den Epsilon-Vergleich möchte man in eine Funktion auslagern, weil sonst der Code schnell zu einem „Wald von diffs“ und zufälligen Konstanten wird. Beginnen wir mit unserem kleinen Tagesprojekt: dem Konsolen‑„Triangle Inspector“, der die Seiten eines Dreiecks einliest und dessen Typ bestimmt. Heute brauchen wir den ersten Baustein — die Vergleichsfunktion.
Almost equal: almostEqual und eigenes abs
Zuerst implementieren wir unser eigenes abs für double (nur mit Basisoperatoren):
double absDouble(double x) {
if (x < 0.0) return -x;
return x;
}
Nun — „fast gleich“:
bool almostEqual(double a, double b, double eps) {
return absDouble(a - b) < eps;
}
Mini‑Check in main:
#include <iostream>
int main() {
const double eps = 1e-12;
double a = 0.1 + 0.2;
std::cout << almostEqual(a, 0.3, eps) << '\n'; // 1 (true)
}
Beachten Sie: eps wird als Parameter übergeben und nicht in der Funktion versteckt. Das ist nützlich, weil verschiedene Teile des Programms unterschiedliche „Genauigkeitsverträge“ haben können.
Kleiner Hinweis für später: die Standardbibliothek bietet std::abs mit vielen Überladungen für verschiedene Typen. Heute implementieren wir bewusst die manuelle Variante, um nicht von der nächsten Vorlesung über <cmath> abhängig zu sein.
Grenzvergleiche: kleiner/größer mit „Spielraum“
Nachdem Sie die Idee „fast gleich“ akzeptiert haben, stellt sich die Frage: wie verhalten wir uns bei Bedingungen wie x < limit? An der Grenze kann es vorkommen, dass eigentlich genau limit erwartet wurde, aber das Ergebnis limit + 1e-16 ist, und das Programm plötzlich in „größer“ geht, obwohl es semantisch dasselbe ist. Deshalb macht man bei Schwellenwerten oft einen „Spielraum“ mittels Epsilon.
Praktische Regeln:
- „streng kleiner“: x < limit - eps
- „streng größer“: x > limit + eps
- „ungefähr gleich“: abs(x - limit) < eps
Das kann man als Funktionen kapseln (damit if-Ausdrücke lesbarer werden):
bool definitelyLess(double a, double b, double eps) {
return a < b - eps;
}
bool definitelyGreater(double a, double b, double eps) {
return a > b + eps;
}
Und ein Mini‑Beispiel:
#include <iostream>
int main() {
const double eps = 1e-12;
double x = 1.0;
double limit = 1.0;
std::cout << definitelyLess(x, limit, eps) << '\n'; // 0
}
Dieser Ansatz ist besonders nützlich in Verzweigungen, bei denen sich das Verhalten des Algorithmus an der Schwelle ändert. Sonst erhält man ein „Flattern“ an der Grenze: mal der eine Zweig, mal der andere — nur wegen eines winzigen Fehlers.
5. Praktisches Beispiel: Wir bestimmen, ob ein Dreieck rechtwinklig ist
Jetzt fügen wir alles in unser Mini‑Programm „Triangle Inspector“ zusammen. Die Aufgabe ist einfach: ein Dreieck ist rechtwinklig, wenn der Satz des Pythagoras gilt. Aber bei double-Seiten kann der Vergleich mit == versagen, insbesondere wenn die Seiten aus Berechnungen stammen (oder als Dezimalzahlen eingegeben wurden oder aus vorherigen Formeln entstanden sind).
Wir müssen prüfen:
\[ a^2 + b^2 \approx c^2 \]
Zuerst machen wir eine kleine „Normalisierung“: wir finden die größte Seite und nennen sie c. Ohne Sortieralgorithmen — einfach mit ein paar if-Anweisungen, da wir uns noch auf Basis‑C++ beschränken.
void sort3(double& a, double& b, double& c) {
if (a > b) { double t = a; a = b; b = t; }
if (b > c) { double t = b; b = c; c = t; }
if (a > b) { double t = a; a = b; b = t; }
}
Rechtwinkligkeitsprüfung mit Epsilon:
bool isRightTriangle(double a, double b, double c, double eps) {
sort3(a, b, c);
return almostEqual(a * a + b * b, c * c, eps);
}
Und jetzt main, das Seiten einliest und das Ergebnis ausgibt:
#include <iostream>
int main() {
double a = 0.0, b = 0.0, c = 0.0;
std::cin >> a >> b >> c;
const double eps = 1e-9;
std::cout << isRightTriangle(a, b, c, eps) << '\n'; // 1 für 3 4 5
}
Wenn Sie 3 4 5 eingeben, erwarten Sie 1 (true). Wenn Sie etwas wie 1 1 1 eingeben, erwarten Sie 0. Und wichtig: wenn die Zahlen aus Berechnungen stammen (z. B. Skalierung, Normalisierung oder Divisionen), dann verhält sich dieser Code vorhersagbar und nicht als „heute ja, morgen nein“.
Warum ist eps hier 1e-9 und nicht 1e-12? Weil wir mit Quadraten arbeiten und die Fehler scheinbar „wachsen“ können. Das ist keine strenge Fehlerabschätzung, sondern eine praktische Wahl, die sich in der Geometrie mit Benutzereingaben oft bewährt.
6. Was tun mit NaN und warum plötzlich alles false ist
Nach der Vorlesung zu NaN ist wichtig zu verstehen: der Epsilon‑Vergleich ist kein „NaN‑Zauber“. Wenn irgendwo in Ihren Berechnungen ein NaN auftaucht, wird auch der Ausdruck a - b zu NaN, dann bleibt absDouble(NaN) NaN, und ein Vergleich wie NaN < eps ergibt false. Das wirkt wie „nichts funktioniert“, ist aber ehrliches Verhalten: mit NaN kann man nicht wie mit normalen Zahlen vergleichen.
Auf Basisniveau kann man eine sehr einfache Diagnose einbauen: eine Zahl x ist NaN, wenn x != x. Jede normale Zahl ist gleich sich selbst, NaN hingegen nicht.
Beispielsweise kann man unsere Funktionen so schützen (ohne das Design zu verkomplizieren):
bool isNaN(double x) {
return x != x;
}
Und vor wichtigen Prüfungen einen frühen Abbruch machen:
bool safeAlmostEqual(double a, double b, double eps) {
if (isNaN(a) || isNaN(b)) return false;
return almostEqual(a, b, eps);
}
Das ist keine „vollständige Absicherung gegen alle Probleme“ (es gibt auch noch Infinity und Domänenbeschränkungen), aber es macht das Programmverhalten verständlicher: wenn die Eingaben schlecht sind, tun wir nicht so, als wäre alles normal.
In der nächsten Vorlesung, wenn wir <cmath> verwenden, gibt es standardmäßigere Werkzeuge für solche Prüfungen, aber vorerst bleiben wir beim Basisstoff.
7. Typische Fehler beim Epsilon‑Vergleich
Fehler Nr. 1: Einmal == durch Epsilon ersetzen — und denken, das Problem sei für alle Zeit gelöst.
Manche setzen abs(a - b) < 1e-12 an einer Stelle ein, lassen an anderer Stelle aber weiterhin if (x == 0.0) oder if (y == limit). Das Programm „flattert“ also weiterhin an den Grenzen, nur etwas seltener. Wenn Sie die Toleranzidee akzeptiert haben, sollten Vergleiche in der Nähe von Berechnungen konsequent angewendet werden.
Fehler Nr. 2: eps ohne Bezug zum Aufgabensinn wählen.
Typischer Fall: jemand hat online 1e-9 gesehen und verwendet nun überall 1e-9, auch bei Prozenten, Entfernungen, Preisen und Geometrie. eps ist Teil der Genauigkeitsanforderungen. Wenn Sie Werte mit zwei Nachkommastellen ausgeben, macht eine Toleranz von 1e-12 das Programm nicht klüger — nur überheblich.
Fehler Nr. 3: das Betragszeichen der Differenz vergessen und stattdessen einfach a - b < eps vergleichen.
Wenn a kleiner als b ist, ist die Differenz negativ und die Bedingung ist fast immer wahr (z. B. -100 < 1e-12 — stimmt). Deshalb braucht der Epsilon‑Vergleich immer den Betrag: |a - b|, entweder manuell oder per Bibliothek.
Fehler Nr. 4: Epsilon nur für „gleich“ nutzen, aber die Grenzen in x < limit nicht berücksichtigen.
Selbst wenn Sie a und b mit Epsilon vergleichen, können Schwellenwertbedingungen an der Grenze versagen. In der Praxis sind Bedingungen wie x < limit - eps und x > limit + eps oft deutlich robuster als das „ehrliche“ x < limit.
Fehler Nr. 5: nicht verstehen, dass NaN Vergleiche in „alles false“ verwandelt.
Wenn irgendwo ein NaN entstanden ist, können Ausdrücke wie abs(a - b) < eps, a < b, oder a >= b plötzlich false zurückgeben und Verzweigungen brechen. Das ist kein Mysterium und kein „Computerdefekt“, das ist die Spezifikation von NaN. Es ist nützlich, schnell prüfen zu können, ob x != x und zu verstehen: „ah, deshalb hat die Bedingung nicht gegriffen“.
GO TO FULL VERSION