Computergrundkenntnisse und Programmieren, WS 07/08, Übung 12: Numerik der Standardbibliothek Bei der Behandlung von physikalischen Problemen spielen numerische Rechnungen eine wichtige Rolle. Die C++ Standardbibliothek bietet dazu einige nützlich Klassen die hier kurz vorgestellt werden. Zusätzlich ist es oft nötig Programme aus Bibliotheken zu verwenden. Dieses Thema wird im letzten Kurs angesprochen. Mathematische Standardfunktionen Wir haben bereits einige Standardfunktionen aus der Headerdatei <cmath> verwendet. Hier eine (fast) vollständige Liste aller darin enthaltenen Funktionen: double double double double double double double double double double double double double double double double double double double abs (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Absolutwert ceil (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kleinster Integer nicht kleiner als x floor (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . Größter Integer nicht größer als x sqrt (double x) . . . . . . . . . . . . . . . . . . . . . . . Quadratwurzel, x darf nicht negativ sein pow (double x, double e) . . . . . . . . . . . . . . . xe . Fehler, falls x == 0 und e <= 0, oder falls x < 0 und e nicht integer. pow (double x, int n) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xn . sin (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sinus cos (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cosinus tan (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tangens asin (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arcus-Sinus acos(double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arcus-Cosinus atan (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arcus-Tangens atan2 (double x, double y) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . atan(x/y) sinh (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sinus-hyperbolicus cosh (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cosinus-hyperbolicus tanh (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tangens-hyperbolicus exp (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exponentialfunktion (Basis e) log (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Logarithmus zur Basis e, x muss größer Null sein. log10 (double x) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Logarithmus zur Basis 10, x muss größer Null sein. Diese Funktionen sind auch für die Typen float und long definiert. Bei Aufruf mit beispielsweise einem int Argument wird, wo möglich, in double konvertiert. Die Technik, dass man einen Funktionsnamen mehrfach vergeben kann und der Compiler die Funktionen anhand des Typs der Argumente identifiziert, nennt man Überladen von Funktionen. Offensichtlich ist dies bei den bereits vorgefertigten mathematischen Standardfunktionen sehr praktisch. Sie können die Technik des Überladens aber auch für selbst geschriebene Funktionen verwenden (siehe Aufgabe 12.2). Ein anderes Beispiel wäre die Berechnung der Gammafunktion: Bei einem ganzzahligen, nichtnegativen Argument n ist die Gammafunktion durch n! gegeben. Sie können also zwei gleichnamige Unterprogramme schreiben, double gamma (int n) und double gamma (double x) die sich nur durch den Typ des Arguments unterscheiden. Wird das Unterprogramm gamma dann mit einem positiven ganzzahligen Argument aufgerufen, dann können Sie die Berechnung auf die Berechnung von n! zurückführen. Der Compiler verwendet die numerisch viel aufwändigere Rechnung der Gammafunktion für reelle Argumente nur dann, wenn das Argument wirklich vom Typ double ist. 1 Komplexe Zahlen Mit der Headerdatei complex werden die Klassen und Unterprogramme zum Arbeiten mit komplexen Zahlen eingebunden. Das nachfolgende Beispielprogramm illustriert die wichtigsten Optionen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <cmath> #include <complex> using namespace std; int main() { complex<double> c1,c2,I; double dd = 2.7; c1 = complex<double> (2,3.5432); c2 = polar(1.0,0.4); I = complex<double> (0,1); cout cout cout cout cout cout cout cout cout cout cout cout cout cout cout << << << << << << << << << << << << << << << endl; " c1 " c2 " I " conj(c1) " c1 + I " c1 + dd " c1 * I " Re c1 " Im c1 " 2.5 * c1 " log(c2) " sqrt(I) " abs(c1) " arg(c2) = = = = = = = = = = = = = = " " " " " " " " " " " " " " << << << << << << << << << << << << << << c1 << endl; c2 << endl; I << endl; conj(c1) << endl; c1+I << endl; c1+dd << endl; c1*I << endl; c1.real() << endl; c1.imag() << endl; 2.5*c1 << endl; log(c2) << endl; sqrt(I) << endl; abs(c1) << endl; arg(c2) << endl; cout << endl << " Deine eigene komplexe Zahl : "; cin >> c1; cout << " Deine Zahl : " << c1 << endl << endl; } Wie aus dem Beispiel hervorgeht können Sie Variablen für komplexe Zahlen mit der Anweisung complex<double> definieren (dabei kann double auch durch float oder long ersetzt werden. Den Variablen kann man dann auf zwei Arten Werte zuweisen: Entweder durch Angabe von Realund Imaginärteil, rr und ii, in der Zuweisung complex<double> (rr,ii) oder durch Angabe von Absolutbetrag und Phase, dd und pp, in der komplexwertigen Funktion polar(dd,pp). Die komplexen Variablen gehorchen den üblichen Rechenregeln für komplexe Zahlen. Insbesondere sind die Operationen + , - , * , / implementiert. Falls bei diesen Verknüpfungen eine der beiden Zahlen eine reelle oder ganze Zahl ist, wird diese korrekt in eine komplexe Zahl umgewandelt. 2 Die reellen Methoden .real() und .imag() liefern Real- und Imaginärteil der komplexen Zahl, die komplexwertige Funktion conj() das komplex konjugierte des Arguments. Die reellwertigen Funktionen abs() und arg() berechnen den Betrag einer komplexen Zahl, beziehungsweise die Phase in ihrer Polardarstellung. Die mathematischen Funktionen sin(z), cos(z), tan(z), sinh(z), cosh(z), tanh(z), exp(z), sqrt(z), log(z), log10(z) sind gemäß ihrer Definition für komplexe Zahlen z implementiert. Zusätzlich dazu gibt es noch die Potenzfunktionen pow(z,n), pow(z,r), pow(z1,z2), pow(r,z), wobei n eine integer-Variable ist und r reell. Eine komplexe Zahl wird im Format (x,y) ausgegeben, und kann in den Formen (x,y), (x) und x eingelesen werden, wobei bei den letzten beiden Möglichkeiten der Imaginärteil Null bleibt. Valarrays: Neben den simplen Arrays die wir am Anfang kennen gelernt haben, und den sehr allgemeinen Containerklassen gibt es eine weitere Vektorstruktur, sogenannte Valarrays. Dies sind eindimensionale Vektoren für int, double oder complex<double> Zahlen (beziehungsweise deren Kurz- oder Langvarianten), die auf hohe Verarbeitungsgeschwindigkeit optimiert sind. Hier ein Beispielprogramm, das den Gebrauch von Valarrays demonstriert: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include #include #include #include <iostream> <cmath> <valarray> <complex> using namespace std; void fill( valarray<int>& vi, valarray< complex<double> >& vc ); // ----------------------------------------------------------------------int main() { valarray<double> uu(4),vv(4),ww(4),zz(4); double su, sv, sw; int nn; nn = uu.size(); cout << endl; cout << "nn = " << nn << endl << endl; for (int i = 0; i < nn; i++) uu[i] = i; vv = uu*uu; ww = vv + uu - 0.1; zz = sin(uu); for (int i = 0; i < nn; i++) cout << uu[i] << " "<< vv[i] << " " << ww[i] << " " << zz[i] << endl; 3 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 su = uu.sum(); sv = vv.sum(); sw = ww.sum(); cout << su << " "<< sv << " " << sw << endl << endl; // ----------------------------------------------------------------------int shilf[] = {2,4,6,8}; valarray<int> ss(shilf,4); valarray< complex<double> > cc; fill(ss,cc); for (int i = 0; i < nn; i++) cout << ss[i] << " "<< conj(cc[i]) << endl; cout << endl; } // ----------------------------------------------------------------------void fill( valarray<int>& vi, valarray< complex<double> >& vc ) { int ni = vi.size(); vc.resize(ni); for (int i = 0; i < ni; i++) vc[i] = complex<double>(i,vi[i]); } Valarrays werden mit dem Anweisung valarray<typ> name(leng) definiert (siehe Zeile 14). Wird bei der Definition die Länge des Vektors weggelassen (Zeile 43), so wird eine Vektor der Länge Null erzeut, dem man später mit der Methode .resize(leng) sein Länge zuweisen kann. Im Gegensatz zu normalen Variablen werden Valarrays bei der Deklaration mit 0 initialisiert. Es gibt aber auch die Möglichkeit ein Valarray mit einem vorgegebenen normalen array gleicher Länge zu initialisieren (siehe Zeilen 40 und 41). Die Methode .size() gibt die Länge des Valarrays als integer Variable an, die Methode .sum() die Summe der Einträge. Die einzelnen Einträge können mit vv[i] angesprochen werden wobei die Variable i wie üblich von i = 0 bis v.size()-1 läuft. Valarrays können sehr einfach manipuliert und miteinander verknüpft werden (Zeilen 25, 26, 27). Jede Operation wird dabei immer punktweise, d.h. für jeden Eintrag einzeln ausgeführt. Es empfiehlt sich, Valarrays bei der Übergabe in ein Unterprogramm explizit mit & für die Übergabe per Referenz zu deklarieren. Dies erlaubt es (wie im Beispiel demonstriert) erst im Unterprogramm die Größe festzulegen. 4 Aufgabe 12.1: Um eine wenig mit den komplexen Zahlen in einem C++ Programm zu spielen betrachten wir eine komplexe Folge zn ∈ C, n = 0, 1, ... mit der Rekursionsgleichung zn+1 = zn2 . (1) Dabei sind offensichtlich die beiden reellen Werte z = 0 und z = 1 Fixpunkte der Rekursionsgleichung, d.h., sie werden von (1) auf sich selbst abgebildet. ist z = 0 ein attraktiver Fixpunkt, während z = 1 repulsiv ist. Das bedeutet, dass im ersten Fall ein von z = 0 etwas unterschiedlicher Startwert schnell wieder zu z = 0 konvergiert, während kleine Änderungen um z = 1 von diesem Wert wegführen. Verwendet man die Polardarstellung z = reiϕ , r, ϕ ∈ R , r ≥ 0 , ϕ ∈ [0, 2π) , (2) so sieht man, dass alle Startwerte z0 mit r < 1 zu z = 0 konvergieren, während die Folge für Startwerte mit r > 0 divergiert. Für Startwerte mit r = 1 und ϕ > 0 rotieren die Folgenwerte auf dem Einheitskreis. Schreiben Sie ein Programm, bei dem Sie einen Startwert z0 in der Polardarstellung eingeben (r und ϕ einlesen). Iterieren Sie dann die Folge (1) nter Verwendung der komplexen Arithmetik von C++ , und geben Sie Real- und Imaginärteil als reelle Zahlenpaare x, y in einen File aus. Zeichnen Sie dann unter Verwendung des in Kurs 7 besprochenen Grafikprogrammes “xmgrace” die Folge für einige Startwerte z0 . Das Ergebnis könnte zum Beispiel so aussehen wie in der Figur unten. 1.0 0.5 0.0 -0.5 -1.0 -1.0 -0.5 0.0 0.5 r = 0.950, ϕ = − 0.100 r = 0.999, ϕ = 0.001 r = 0.900, ϕ = 1.500 r = 1.001, ϕ = − 0.020 5 1.0 Aufgabe 12.2: Die Norm eines Vektors v mit n reellen Einträgen ist gegeben durch v u n uX ||v|| = t vi2 , (3) v u n uX ||v|| = t vi∗ vi . (4) i=1 und bei komplexen Einträgen durch i=1 Schreiben Sie zwei reellwertige Unterprogramme, double norm( valarray<double>& vv ) und double norm( valarray< complex<double> >& vv ), die jeweils ein Valarray (einmal reell, einmal komplex) übergeben bekommen und die Norm berechnen. Der übergebene Vektor darf dabei nicht geändert werden und Sie sollen, wo möglich, die Methoden .size() und .sum() verwenden. Testen Sie die beiden Unterprogramme in einem gemeinsamen Hauptprogramm um auch das Überladen von Funktionen zu üben. 6