Diskussion:C++-Programmierung/ Einführung in C++/ Rechnen (lassen)
Abschnitt hinzufügenLösung der Aufgabe
[Bearbeiten]Dies ist die Lösung der Aufgabe:
// Dieses Programm loest folgende Aufgaben: // Zahl = (500-100*(2+1))*5 // Zahl = (Zahl-700)/3 // Zahl += 50*2 // Zahl *= 10-8 // Zahl = ++Zahl - Zahl-- #include <iostream> using namespace std; int main() { int zahl; zahl = ( 500 - 100 * ( 2 + 1 ) ) * 5; cout << "( 500 - 100 * ( 2 + 1 ) ) * 5 = " << zahl << "\n"; cout << "( " << zahl; zahl = ( zahl - 700 ) / 3; cout << " - 700 ) / 3 = " << zahl << "\n"; cout << zahl; zahl += 50*2; cout << " + 50 * 2 = " << zahl << "\n"; cout << zahl; zahl *= 10-8; cout << " * ( 10 - 8 ) = " << zahl << "\n"; cout << "++" << zahl << " - " << zahl << "-- = "; zahl = ++zahl - zahl--; cout << zahl << "\n"; }
--etlam 08:53, 9. Aug. 2007 (CEST)
- Dieses Programm zeigt undefiniertes Verhalten, weil es eine Variable (
zahl
) ohne dazwischenliegenden Sequenzpunkt (sequence point) mehrfach ändert (inzahl = ++zahl - zahl--
wirdzahl
gleich dreimal geändert: durch++
, durch--
und durch=
). Das Ergebnis dieser Operation wird in der Praxis vom Compiler und von den Optimierungseinstellungen abhängen, garantiert wird aber nichteinmal, dass überhaupt etwas herauskommt: Undefiniertes Verhalten bedeutet, dass der Compiler produzieren darf, was er will. - Oder kurz gesagt: Das Programm ist fehlerhaft. Die Aufgabenstellung sollte auch entsprechend abgeändert werden, da sie die Erstellung eines fehlerhaften Programms mindestens nahelegt. --Ce 14:12, 9. Aug. 2007 (CEST)
- Vielen dank für den Hinweiß, ich werde mich in kürze darum kümmern. An dieser Stelle möchte ich jedoch kurz auf folgendes Hinweisen (für alle die das schon lesen können):
// Dieses Programm loest folgende Aufgaben:
// Zahl = (500-100*(2+1))*5
// Zahl = (Zahl-700)/3
// Zahl += 50*2
// Zahl *= 10-8
// Zahl = ++Zahl - Zahl--
#include <iostream>
using namespace std;
class test;
ostream& operator<<(ostream &os, test const &z);
class test{
public:
test(){}
test(int i):z(i){}
test(test const& cpy):z(cpy.z){}
test& operator=(test const& cpy){z=cpy.z;}
test& operator++(){++z;return *this;}
test& operator--(){--z;return *this;}
test const operator++(int){test tmp(*this); z++; return tmp;}
test const operator--(int){test tmp(*this); z--; return tmp;}
test operator+(test const& r){return test(z)+=r;}
test operator-(test const& r){return test(z)-=r;}
test operator*(test const& r){return test(z)*=r;}
test operator/(test const& r){return test(z)/=r;}
test& operator+=(test const& cpy){z+=cpy.z;return *this;}
test& operator-=(test const& cpy){z-=cpy.z;return *this;}
test& operator*=(test const& cpy){z*=cpy.z;return *this;}
test& operator/=(test const& cpy){z/=cpy.z;return *this;}
private:
int z;
friend ostream& operator<<(ostream &os, test const &z);
};
ostream& operator<<(ostream &os, test const &z){
os << z.z;
}
int main()
{
{
int zahl;
zahl = ( 500 - 100 * ( 2 + 1 ) ) * 5;
cout << "( 500 - 100 * ( 2 + 1 ) ) * 5 = " << zahl << "\n";
cout << "( " << zahl;
zahl = ( zahl - 700 ) / 3;
cout << " - 700 ) / 3 = " << zahl << "\n";
cout << zahl;
zahl += 50*2;
cout << " + 50 * 2 = " << zahl << "\n";
cout << zahl;
zahl *= 10-8;
cout << " * ( 10 - 8 ) = " << zahl << "\n";
cout << "++" << zahl << " - " << zahl << "-- = ";
zahl = ++zahl - zahl--;
cout << zahl << "\n";
}
cout << '\n';
{
test zahl;
zahl = ( 500 - 100 * ( 2 + 1 ) ) * 5;
cout << "( 500 - 100 * ( 2 + 1 ) ) * 5 = " << zahl << "\n";
cout << "( " << zahl;
zahl = ( zahl - 700 ) / 3;
cout << " - 700 ) / 3 = " << zahl << "\n";
cout << zahl;
zahl += 50*2;
cout << " + 50 * 2 = " << zahl << "\n";
cout << zahl;
zahl *= 10-8;
cout << " * ( 10 - 8 ) = " << zahl << "\n";
cout << "++" << zahl << " - " << zahl << "-- = ";
zahl = ++zahl - zahl--;
cout << zahl << "\n";
}
}
- Ausgabe von g++ 4.1:
( 500 - 100 * ( 2 + 1 ) ) * 5 = 1000 ( 1000 - 700 ) / 3 = 100 100 + 50 * 2 = 200 200 * ( 10 - 8 ) = 400 ++400 - 400-- = -1 ( 500 - 100 * ( 2 + 1 ) ) * 5 = 1000 ( 1000 - 700 ) / 3 = 100 100 + 50 * 2 = 200 200 * ( 10 - 8 ) = 400 ++400 - 400-- = 0
- Offensichtlich gilt die festgelegte Reihenfolge für Operatorauswertung nicht für die Grunddatentypen. (Ich hatte erwartet das auch beim Rechnen mit einem int 0 das Ergebnis ist.) Kann bitte jemand nachprüfen ob dies nach C++-Standard zulässig ist und wenn ja, warum. Insbesondere ist natürlich interessant, ob es sich hier tatsächlich nur um eine "Sonderregelung" für Grunddatentypen gibt (was für mich zumindest noch ein bisschen Logik hätte), oder ob das Verhalten für Klassen ebenfalls undefiniert ist.
- Auf dieses Problem muss natürlich hingewiesen werden. --Prog 20:21, 9. Aug. 2007 (CEST)
- Ich habe gerade noch mal darüber nachgedacht und bin zu dem Schluss gekommen, dass es durchaus logisch ist. Die Erklärung dazu folgt allerdings erst morgen Abend, da ich jetzt zu müde bin. --Prog 22:36, 9. Aug. 2007 (CEST)
- (Gleiche Einrückung wegen Bearbeitungskonflikt; jetzt alles noch mal einzurücken, ist mir zu mühsam)
- Benutzerdefinierte Operatoren unterscheiden sich von eingebauten dadurch, dass ihr Aufruf ein Funktionsaufruf ist, und daher zusätzliche Sequenzpunkte einführt. Dadurch bekommen einige Ausdrücke, die bei eingebauten Operatoren undefiniert sind, einen definierten Wert. Betrachten wir das einmal an dem fraglichen Ausdruck:
zahl = ++zahl - zahl--;
- Hier haben wir eine Zuweisung, deren rechter Operand eine Summe ist, deren Summanden wiederumein Prä-Inkrement und ein Post-Dekrement sind.
- Betrachten wir zunächst einmal den Fall mit eingebauten Operatoren (z.B. mit
int
): - Der Ausdruck ist ein Zuweisungsausdruck. Sein Ergebnis ist der Wert des Ausdrucks auf der rechten Seite, umgewandelt in den Typ der linken Seite, und als Nebeneffekt setzt er die Variable der linken Seite auf den neuen Wert. Es mag merkwürdig erscheinen, das als Nebeneffekt zu bezeichnen, wo das doch eigentlich der Hauptzweck der Zuweisung ist, aber im Rahmen des C++-Stabndards sind alle Veränderungen an Variablen Nebeneffekte. Das wichtige an einem Nebeneffekt ist nun, dass nicht genau festgelegt ist, wann genau er ausgeführt wird. Alles, was garantiert wird, ist, dass er nach dem vorgehenden und vor dem nachfolgenden Sequenzpunkt durchgeführt wird (und bei Zuweisungen zusätzlich, dass jedes Lesen der Variablen auf der rechten Seite des Gleichheitszeichens vor der Änderung erfolgt, so dass z.B. in
a=a+1
rechts der alte Wert der Variablena
gelesen wird (es ist auch schwer vorstellbar, dass man das anders machen könnte). Die einzigen Sequenzpunkte sind in diesem Fall der Anfang und das Ende des Statements. - So, nun betrachten wir das, was hier zugewiesen wird. Es handelt sich um die Differenz zweier Ausdrücke. Es ist natürlich klar, daß die Ausdrücke ausgewertet werden, bevor sie subtrahiert werden, aber es ist nicht festgelegt, in welcher Reihenfolge sie ausgewertet werden (anders als z.B. in Java, wo der linke Ausdruck vollständig ausgewertet werden muss, bevor der rechte ausgewertet wird). Es ist also nicht festgelegt, ob zuerst
++zahl
oder
zahl-- ausgewertet wird. Aber es kommt noch schlimmer: Schauen wir uns die beiden Summanden genauer an. Der Ausdruck vor dem Minus ist ein Präinkrement-Ausdruck. Das heisst, sein Wert ist um eins mehr als der Wert des Arguments (also der Variablen
zahl
), und als Nebeneffekt setzt er das Argument auch auf diesen Wert. Zur Erinnerung: Nebeneffekte können zu einem beliebigen Zeitpunkt zwischen den Sequenzpunkten stattfinden. Dasselbe gilt für den Ausdruck nach dem Minus: Der Post-Dekrement-Operator liefert den aktuellen Wert des Arguments und erhöht als Nebeneffekt den Wert desselben um 1. Betrachten wir nun wieder den Audruck als Ganzes, dann stellen wir fest, daß die Variable zweimal gelesen wird (einmal in jedem Teilausdruck), und dreimal per Nebeneffekt geschrieben (je einmal durch den Prä-Inkrement-, den Post-Dekrement- und den Zuweisungsoperator). Der Zuweisungsoperator garantiert, daß seine Änderung erst nach den Lese-Operationen passiert, aber das ist die einzige Garantie, die man hier bekommt. Wenn wir mal den erzeugten Code als Pseudo-C++ schreiben, und die Prozessor-Register durch Variablenreg1
etc. darstellen, dann entspricht der Code, an den Du vermutlich gedacht hast, folgendem:
// Präinkrement
reg1 = zahl; // (1) lesen für ++zahl
reg1 = reg1 + 1; // (2) erhöhen um 1
zahl = reg1; // (3) schreiben für ++
reg2 = zahl; // (4) lesen für zahl--
reg3 = reg2 + 1; // (5) erhöhen um 1, alten Wert merken
zahl = reg3; // (6) schreiben für --
reg4 = reg1 - reg2; // (7) Differenz
zahl = reg4; // (8) Schreiben für "="
- Mit dieser Implementierung wird der Ausdruck effektiv zu
zahl = zahl + (zahl+1)
.
- Aber der Standard garantiert nur:
- (1) vor (2) vor (3)
- (4) vor (5) vor (6)
- (2) vor (7)
- (5) vor (7)
- (7) vor (8) (impliziert insbesondere (1) vor (8) und (4) vor (8))
- Wie man sieht, hat der Compiler hier viele Freiheiten zum Umsortieren, mit zum Teil unterschiedlichen Ergebnissen. Als einfaches Beispiel: Wenn (3) hinter (8) verschoben wird, der Rest aber gleich bleibt, dann wird der Ausdruck effektiv zu
zahl=zahl+1
.
- Aber es wird noch schlimmer: Wie wir gesehen haben, wird hier zwischen zwei Sequenzpunkten dieselbe Variable mehrfach geschrieben, und der Standard sagt explizit, dass dies zu undefiniertem Verhalten führt (wie übrigens auch schon das Lesen und Schreiben derselben Variablen, wenn der gelesene Wert nicht zur Berechnung des geschriebenen Wertes benötigt wird;
i + i++
ist also auch schon undefiniert). Das bedeutet, der Compiler ist noch nicht einmal an die oben angegebenen Möglichkeiten gebunden. Egal, was für Code der Compiler hier erzeugt, er ist immer standardkonform! Und je besser die Optimierungen der Compiler werden, desto wahrscheinlicher wird es, dass nach Standard undefinierter Code tatsächlich unerwartetes (und scheinbar unerklärliches) Verhalten zeigt.
- Ok, aber warum funktioniert dann alles so wunderbar, wenn man benutzerdefinierte Operatoren verwendet? (Ganz so wunderbar funktioniert es auch wieder nicht, aber dazu kommen wir noch.)
- Nun, wie schon gesagt, sind benutzerdefinierte Operatoren Funktionen. Nun haben Funktionen die Eigenschaft, sowohl beim Aufruf als auch beim Rücksprung einen Sequenzpunkt zu enthalten; hinzu kommt noch, daß Funktionen getrennt voneinander ausgeführt werden, sprich, solange eine Funktion läuft, ist die restliche Auswertung des Ausdrucks sozusagen ausgesetzt. Die Änderungen werden nun innerhalb der Operator-Funktionen ausgeführt, so dass sie vom Rest durch die Funktions-Sequenzpunkte "abgeschottet" sind. Dadurch ist also garantiert, dass für den benutzerdefnierten operator++ der Inkrement und das Ändern des Wertes "in einem Rutsch" passieren.
- Allerdings gibt es auch hier noch eine Freiheit: Die Reihenfolge, mit der die Argumente des Minus-Operators ausgewertet werden, ist auch hier nicht festgelegt (allerdings ergibt dies kein undefiniertes Verhalten, sondern nur zwei verschiedene mögliche Ergebnisse, je nach Reihenfolge). Die Differenz ist batürlich schlecht geeignet, um den Unterschied zu erkennen: Im ersten Fall wird erst inkrementiert, dann zweimal der Wert gelesen, dann dekrementiert, im zweiten Fall wird erst der Wert gelesen, dann dekrementiert, dann inkrementiert, und schließlich wieder der Wert gelesen. In beiden Fällen wird also jeweils zweimal derselbe Wert gelesen (der aber im ersten Fall um 1 höher ist als im zweiten), die Differenz ist also in beiden Fällen 0.
- Das hat übrigens nicht wirklich etwas mit benutzerdefinierten Typen zu tun; der folgende Code zeigt auch mit
int
dasselbe Verhalten wie die obige Klasse:
#include <iostream>
#include <ostream>
int preincrement(int& i) { return ++i; }
int postdecrement(int& i) { return i--; }
int main()
{
int zahl = 300;
zahl = preincrement(zahl) - postincrement(zahl);
std::cout << zahl << std::endl; // gibt immer 0 aus
}
- Der folgende Code hingegen hat wiederum undefiniertes Verhalten:
#include <iostream>
#include <ostream>
int identity(int i) { return i; }
int main()
{
int zahl = 300;
zahl = identity(++zahl) - identity(zahl--);
std::cout << zahl << std::endl;
}
- Auf den ersten Blick sieht es so aus, als ob die Sequenzpunkte der Funktion
identity
die Änderungen an zahl
voneinander abschotten sollte. Allerdings müssen Teilausdrückke nicht zusammenhängend ausgeführt werden (solange es keine Funktionen sind). Der Compiler darf also durchaus erst ++zahl
auswerten, dann zahl--
, und dann erst die beiden Aufrufe von identity
, wodurch wiederum kein Sequenzpunkt zwischen den Änderungen liegt. Der Nebeneffekt der Zuweisung ist allerdings durch die Sequenzpunkte "abgeschirmt", der Ausdruck zahl = identity(zahl++)
ist also wohldefiniert, im Gegensatz zu zahl=zahl++
. --Ce 22:44, 9. Aug. 2007 (CEST)
- Das war sehr Aufschlussreich, vielen Dank. Ich hatte das ganze gestern noch von einer anderen Warte aus betrachtet. Ich habe mir die Operatoren als Funktionsaufrufe vorgestellt. Für die test-Klasse kämme dann etwa folgendes heraus:
// zahl = ++zahl - zahl--;
zahl.operator=(zahl.operator++().operator-(zahl.operator--(0)));
- Dies hält natürlich die von mir erwartete Reihenfolge ein, da keine der Funktionen mehr als ein Argument besitzt und somit auch keine Wahl für den Compiler bleibt, er muss in dieser Reihenfolge auswerten. Betrachten wir hingegen das gleich für einen int, so könnte die Implementierung lauten:
// zahl = ++zahl - zahl--;
// Pseudocode
operator=(zahl, operator-(operator++(zahl), operator--(zahl, 0));
- In diesem Fall hat der Compiler die freie Auswahl, in welcher Reihenfolge er die Argumente der Funktionen mit 2 Argumenten auswertet. Damit ergibt sich eine ähnlich Sicht, wie jene die mein Vorredner dargelegt hat. Eine Frage bleibt mir allerdings noch zu stellen, wenn ich Ce Richig verstanden habe, steht es dem Compiler frei das In- und Dekrementieren (oder besser das zurückschreiben nach Zahl -> (3) und (6)) noch nach der Zuweisung zu machen. Ist das richtig so? --Prog 21:36, 10. Aug. 2007 (CEST)
- Das ist in der Tat richtig so. Oder er könnte das erste Zurückschreiben (3) nach dem Lesen für
operator--
(4) machen, und das zweite Zurückschreiben für operator++
(6) nach dem Schreiben für operator=
(8). Allerdings hat der Compiler hier sogar noch mehr Freiheiten, da der Standard, wie beschrieben, mehrere Änderungen an derselben Variablen ohne einen Sequenzpunkt dazwischen (und auch das Lesen uns Schreiben derselben Variablen, solange das Lesen nicht der Feststellung des geschriebenen Wertes dient) explizit als undefiniertes Verhalten festlegt, der Compiler darf an dieser Stelle also im Prinzip alles.
- Übrigens ist Deine Annahme, bei den Operatoren in Memberfunktions-Implementierung gäbe es nur ein Argument, auch falsch: Der Aufruf
a.foo(b)
hat ebenfalls 2 Argumente: Das erste Argument, a
, wird als impliziter this
-Parameter übergeben, das zweite, b
, normal über die Argumentliste. Und der Compiler ist auch hier frei, die Argumente in beliebiger Reihenfolge aufzurufen. Beispielsweise ist im Teilausdruck
zahl.operator++().operator-(zahl.operator--(0))
- das erste Argument von
operator-
(das implizite this-Argument) zahl.operator++()
, und das zweite Argument ist zahl.operator--(0)
. Der Compiler ist auch hier frei in der Entscheidung, welchen der Teilausdrücke er zuerst auswertet (natürlich müssen beide ausgewertet sein, bevor operator-
aufgerufen wird). Aber wie gesagt, bei der Differenz sieht man den Unterschied nicht, da zahl-zahl und (zahl+1)-(zahl+1) denselben Wert (nämlich 0) ergeben. Ein Beispiel, wo man den Irrtum gut sehen kann, ist folgendes:
test i = 1; // mit Deiner Klasse von oben, um undefiniertes Verhalten zu vermeiden
std::cout << i++ << ", " << i++ << "\n";
- Intuitiv würde man hier die Ausgabe "1, 2" erwarten; viele Compiler werden aber ein Programm erzeugen, das "2, 1" ausgibt (solange man die Klasse benutzt, also kein undefiniertes Verhalten produziert, sind das auch die einzigen beiden erlaubten Ausgaben).
- PS: In meinem gestrigen Text habe ich teilweise + und - vertauscht, ich hoffe, das hat nicht zu Verwirrung geführt ... --Ce 17:50, 11. Aug. 2007 (CEST)
Reihenfolge der Auswertung von Ausdrücken
[Bearbeiten]
Also der Standard legt nicht fest, in welcher Reihenfolge die Teilausdrücke eines Ausrucks wie (a+b) ausgewertet werden, etwa wenn a und b Funktionsaufrufe mit Seiteneffekten sind, wie sie nacheinander oder gar gleichzeitig (bei Multithreading oder SMP) aufgerufen werden. Es ist im Normalfall auch völlig irrelevant, da der Standard zwischen zwei Sequence points mehrfache Schreibzugriffe auf die gleiche Variable als "undefined behavior" erklärt, somit ist einem Compiler da sehr viel Freiraum für mögliche Optimierungen gegeben. Soweit ich mich erinnere erfolgen Schreibzugriffe erst nach allen Lesezugriffen auf eine Variable. Die genauen Quellen im Standard kann ich gerne raussuchen, aber nicht mehr heute abend/nacht ;-)) --RokerHRO 00:27, 23. Aug. 2007 (CEST)
"Ausgabe" ?
[Bearbeiten]
Im Abschnitt "Einfaches Rechnen" steht bei "Ausgabe":
1 Benutzereingabe: 774
2 Benutzereingabe: 123
3 774 + 123 = 897
Der Quellcode enthält aber nichts, das
"Benutzereingabe: "
auf dem Bildschirm ausgeben würde.
Man sollte gerade Anfänger nicht damit verwirren, 'Hinweise' mit reinzumischen in die Ausgabe.
Also: Ich finde, "Benutzereingabe: " sollte gelöscht werden - oder noch besser: irgendwie 'angemerkt' werden, sodass (optisch) deutlich wird, dass dieses Wort _nicht_ auf dem Bildschirm erscheinen wird.
Alternativ: Den Quellcode erweitern, sodass "Benutzereingabe: " auch tatsächlich ausgegeben wird ;-)