Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 19.04.2017 Mathematik und ihre Anwendung am Lehrstuhl Prof. Thomas Schuster www.num.uni-sb.de/schuster Forschungsschwerpunkte der AG Schuster Allgemein: Inverse Probleme (Theorie und Anwendungen) Numerische Analysis Optimierung Numerik partieller Differentialgleichungen Anwendungen: Vektortomographie (2D, 3D) Parameteridentifizierungen bei anisotropen, elastischen Wellengleichungen Terahertz-Tomographie Magnetpartikelbildgebung Hyperspektrale Bildgebung Algorithmenentwicklung in der Terahertz-Tomographie Zerstörungsfreie Prüfung eines Polyethylenblocks: Standardmethode und verbesserte Variante Numerisches Praktikum in der Computertomographie (9 / 12 CP) Inhalt der Vorlesung Phänomen, reale Welt Experiment Hypothese Vereinfachung Abstraktion Erfahrung Modellfehler (Mathematisches) Modell (Mathematisch) exakte Formulierung eines Problems Vergleich Evaluierung Ergebnis Modellverfeinerung Parameteradaption Implementierung des Programms Stabilität des Algorithmus Lösbarkeit und Kondition des Problems Syntaktische und semantische Fehler Rundungsfehler Verfahrensfehler Datenfehler Die Programmiersprache C Definition (Programmiersprache) Eine Programmiersprache ist eine künstliche Sprache, die entwickelt wurde, um Rechenvorschriften für eine Maschine, in der Regel einen Computer, zu formulieren. Kommunikationsmittel zwischen Mensch und Maschine Vokabeln und Grammatik (Syntax) >1800 Sprachen mit verschiedenen Intentionen Warum C? ursprünglich zur Entwicklung von Betriebssystemen heute auch häufig in Anwendungssoftware zu finden Compiler verfügbar für nahezu alle Prozessoren und Betriebssysteme hardwarenah, erlaubt direkten Speicherzugriff → schnell!! weit verbreitet, viele kostenlose Bibliotheken oder Programmbausteine im Netz Grundlage für C++, Java, Python, Perl, PHP,... Die integrierte Entwicklungsumgebung KDevelop Kostenlos verfügbar unter Linux – Open Source Software Stabil – von zahlreichen Nutzern getestet Viele nützliche Helferlein wie Syntax Highlighting, Auto-Vervollständigung, automatische Einrückung, integrierte Konsole, Blockausblendung, Quelltext-Browser, Hintergrund-Parser, Debugger und vieles mehr... Mein erstes C-Programm $ kdevelop & hallo_welt.c 1 2 3 4 5 6 /* hallo_welt.c - gibt Begruessung auf dem Bildschirm aus */ main() // Achtung: C ist case-sensitive! { printf("Hallo Welt!\n"); } $ gcc hallo_welt.c -o hallo_welt hallo_welt.c: In Funktion ’main’: hallo_welt.c:3:3: Warnung: Unverträgliche [...] Funktion ’printf’ $ ls hallo_welt hallo_welt.c $ ./hallo_welt Hallo Welt! hallo_welt.c~ Wie entsteht ein Programm? prog.c Quellcode Präprozessor Quellcode Compiler prog.o Objektcode Linker prog prog.exe Programm Objektcode library.so Die Programmiersprache C Vokabeln reservierte Wörter: printf, scanf, struct, for, while, float, ... Opertoren: +, &, %, <=, !, *, / Grammatik (Syntax) a + 3 = b; E b = a + 3; printf("Hallo Welt!") float 2wurzelx-1; E X printf("Hallo Welt!"); float cMOU_2Bi5nUOz5O_bG6toBUZb; E a = 5; b = 5 / a; Bedeutung (Semantik) a = 0; b = 5 / a; X E X X Die Software Matlab von MathWorks Warum Matlab? High-Level Sprache: ,→ kurze Entwicklungszeiten Vielfältige Visualisierungsmöglichkeiten MATLAB-Programme sind vollständig portierbar. Integration zusätzlicher Toolboxen (PDE, Optimization, Wavelet) Matlab ist eine kommerzielle Software, aber es gibt eine MathWorks TAH Campuslizenz Die Software darf von allen Studierenden der Universität des Saarlandes genutzt werden. Das umfasst einerseits beliebig viele Installationen auf dem Campus und andererseits auch die Nutzung auf privaten Computern. Notwendig: Registrierung bei der Firma ASKnet AG. Weitere Informationen: https://unisb.asknet.de/cgi-bin/program/S1552 Weitere Informationen: http://www.hiz-saarland.de/informationen/arbeitsplatz/sw-lizenzen/ mathworks-tah-campuslizenz/ Literatur: Internet Skripte Gerald Kempfer: Progammieren in C - Vorlesungsbegleitendes Skript http://public.beuth-hochschule.de/~kempfer/skript_c/c.pdf Wikibooks: C-Programmierung http://upload.wikimedia.org/wikibooks/de/8/8d/CProgrammierung.pdf Springerlink: http://www.springerlink.com Ralf Kirsch, Uwe Schmitt (Programmieren in C, 2007) Manfred Dausmann, Ulrich Bröckl, Joachim Goll (C als erste Programmiersprache, 2008) Jörg Birmelin, Christian Hupfer (Elementare Numerik für Techniker, 2008) Literatur: Hardcopy Semesterapparat Programmierung Fachschaft: Vorlesungsmitschriften Bücher Ralf Kirsch, Uwe Schmitt: Programmieren in C. Eine mathematikorientierte Einführung, Springer 2007 (zurzeit vergriffen, 2. Auflage erscheint 2013) Helmut Erlenkötter: C Programmieren von Anfang an, Rowohlt 1999 Brian Kernighan, Dennis Ritchie: Programmieren in C, Hanser 1990 Manfred Dausmann, Ulrich Bröckl, Joachim Goll: C als erste Programmiersprache, Teubner 2008 Vom Quellcode zum ausführbaren Programm Schritt 1: Code schreiben Quellcode ist eine Textdatei mit der Endung .c Ungeeignet: Textverarbeitungsprogramme wie MS Word oder OpenOffice Writer Geeignet: MS Notepad (Editor), Emacs, gedit, nedit, KDevelop, Eclipse . . . Schritt 2: Code compilieren Kommandozeile starten und in das Verzeichnis wechseln, das den Code enthält Befehl gcc Quellcode.c -o Programm in eine Kommandozeile eingeben Der Compiler gcc übersetzt den Quellcode in Maschinencode, den der Computer versteht. Schritt 3: Programm ausführen Das compilierte Programm wird mit dem Befehl ./Programm aufgerufen. Ohne das vorangestellte ./ liefert das System die Fehlermeldung bash: Programm: Kommando nicht gefunden. Zahlendarstellung: Natürliche Zahlen 241012 = 2 · 105 + 4 · 104 + 1 · 103 + 0 · 102 + 1 · 101 + 2 · 100 = N X cj B j j=0 mit B = 10 (→ Dezimaldarstellung) N=5 c = (c5 , c4 , c3 , c2 , c1 , c0 ) = (2, 4, 1, 0, 1, 2) , cj ∈ {0, . . . , B − 1} 24 = 1 · 16 + 1 · 8 + 0 · 4 + 0 · 2 + 0 · 1 = 1 · 24 + 1 · 23 + 0 · 22 + 0 · 21 + 0 · 20 = N X cj B j j=0 mit B=2 (→ Binärdarstellung) N=4 c = (c4 , c3 , c2 , c1 , c0 ) = (1, 1, 0, 0, 0)|2 , cj ∈ {0, 1} Zahlendarstellung: Ganze Zahlen Binärsystem (B = 2): cN wird als Vorzeichen interpretiert. Beispiel: B = 2, N = 7 −53 = (−1)1 · (0 · 26 + 1 · 25 + 1 · 24 + 0 · 23 + 1 · 22 + 0 · 21 + 1 · 20 ) → c = (1, 0, 1, 1, 0, 1, 0, 1) Frühe Computer verwendeten diese Darstellung Nachteil: 2 Versionen der Null, Ganzzahlarithmetik ineffizient Heute üblich: „Zweierkomplement“ → Eindeutige Darstellung der Null → Effizientere Arithmetik, da keine Fallunterscheidung nach Vorzeichen nötig → Näheres unter en.wikipedia.org/wiki/Two’s_complement,de.wikipedia.org/wiki/Zweierkomplement Datentypen Definition Ein Datentyp ist festgelegt durch einen Wertebereich und die darauf anwendbaren Operationen. Datentyp char int float double "Wertebereich" Zeichen (bspw. Buchstaben und Ziffern) ganze Zahlen Gleitkommazahlen mit einfacher Genauigkeit Gleitkommazahlen mit doppelter Genauigkeit Typmodifizierer short long long long unsigned erlaubt für int int, double int int, char, short int, long int, long long int Datentypen Datentyp char unsigned char int unsigned (int) long (int) unsigned long long long unsigned long long Größe 1 Byte (=8 Bit) 1 Byte 4 Byte 4 Byte 4 Byte∗ 4 Byte∗ 8 Byte 8 Byte kleinster Wert −27 = −128 0 −231 0 wie int wie unsigned −263 0 größter Wert 27 − 1 = 127 28 − 1 = 255 231 − 1 232 − 1 wie int wie unsigned 263 − 1 264 − 1 ∗ Gilt für 32-Bit-Systeme (Linux/Windows/Mac) sowie 64-Bit-Windows. Auf 64-Bit-Linux- oder Mac-Systemen hat long eine Größe von 8 Byte = 64 Bit. Datentyp float double long double Größe 4 Byte 8 Byte 10 Byte∗∗ betragsmäßig kleinster Wert ≈ 1.18 · 10−38 ≈ 2.23 · 10−308 ≈ 3.36 · 10−4932 betragsmäßig größter Wert ≈ 3.40 · 1038 ≈ 1.80 · 10308 ≈ 1.19 · 104932 ∗∗ Nicht eindeutig festgelegt. Der ISO-Standard verlangt lediglich, dass long double mindestens die gleiche Präzision aufweist wie double. Die meisten Compiler interpretieren long double als 80-Bit-Gleitkommazahl. Variablendeklaration Definition Eine Variablendeklaration besteht aus der Angabe eines Datentyps sowie einer Liste von Variablennamen. Datentyp Variablenname1, Variablenname2,...,VariablennameN; vor der ersten Verwendung zu Beginn eines Anweisungsblocks Unterscheidung zwischen Groß- und Kleinschreibung keine Sonderzeichen, wie z.B. #, ß, % keine Ziffern zu Beginn max. Variablennamenlänge: 31 Zeichen Positiv-Beispiele: int i, j, k; float u, w = -3.14, x, y = 1.0, z; unsigned int N = 5; double s; Variablendeklaration Negativ-Beispiele: int dummy#; Fehler: verirrtes # im Programm int dummy%; Fehler: expected =, ,, ; ... int i, double dummy; Fehler: expected identifier or ( before double int i; double 2dummy; Fehler: ungültiger Suffix dummy an Ganzzahlkonstante unsigned double x; Fehler: both unsigned and double in declaration specifiers „Feste Variablen“: const const Datentyp Variablenname = Wert; Auf die entsprechende Variable kann nur noch lesend zugegriffen werden. Bsp.: const int k = 1; .. . k = 2; → Fehler: Zuweisung der schreibgeschützten Variable k Operatoren Definition Anweisung/Manipulation/Rechnung mit festen Regeln wirkt auf einen oder mehrere Operanden unärer Operator: 1 Operand binärer Operator: 2 Operanden Stellung des Operators: Präfixform: Operator steht vor Operanden Suffixform: Operator steht hinter Operanden Infixform: Operator steht zwischen Operanden Zuweisungsoperator ’=’ int a; float x = 3.14, y, z; a = -4; z = y = x; // aequivalent: z = (y = x); binär, Infixstellung rechtsassoziativ ( ← ) Operatoren: Arithmetische Operatoren Arithmetische Opertoren ( + , - , * , / , % ) Name Minus (unär) Plus Minus (binär) Multiplikation Verwendung -Op1 Op1 + Op2 Op1 - Op2 Op1 * Op2 Division Op1 / Op2 Modulo Op1 % Op2 Operandentyp int, float int, float int, float int, float int float int < Resultat Vorzeichenwechsel Summe Differenz Produkt ganzzahlige Division Quotient Rest bei ganzzahliger Division Regeln Sind die Operanden vom gleichen Datentyp, so auch das Ergebnis Sind die Operanden von verschiedenen Typen, so ist das Resultat vom „genaueren“Datentyp: ’int’ + ’double’ = ’double’ Arithmetische Operatoren sind linksassoziativ ( → ) , d.h. a + b + c = (a + b) + c Priorität bei binären Operatoren: „Punkt vor Strich“ Operatoren: Arithmetische Zuweisung und Inkrement Arithmetische Zuweisungsoperatoren Operation Op1 += Op2; Op1 -= Op2; Op1 *= Op2; Op1 /= Op2; Op1 %= Op2; Bezeichnung Additionszuweisung Subtraktionszuweisung Multiplikationszuweisung Divisionszuweisung Modulozuweisung Äquivalent Op1 = Op1 Op1 = Op1 Op1 = Op1 Op1 = Op1 Op1 = Op1 zu + Op2; - Op2; * Op2; / Op2; % Op2; Inkrement- und Dekrementoperatoren ++ und -Für eine Variable i vom Typ int sind äquivalent: i = i+1; ⇐⇒ i += 1; ⇐⇒ i++; i = i-1; ⇐⇒ i -= 1; ⇐⇒ i--; Unterscheidung Postfix- und Präfixnotation sum += i++; ⇐⇒ sum = sum + i; i = i + 1; sum += ++i; ⇐⇒ i = i + 1; sum = sum + i; Operatoren: Arithmetische Zuweisung und Inkrement int a = 3, b, c; b = ++a * 3; // a = 4, b = 12 c = a++ * 3; // c = 12, a = 5 ist äquivalent zu int a = 3, b, c; a++; // oder ++a; b = a * 3; c = a * 3; ++a; // oder a++; Publikumsfrage: Seien a und b vom Typ int. Was ist zu a) -b = a - 1; b) b -= (-a); c) b d) b -= (--a); = -(a--); b -= (a-1); äquivalent? Operatoren: Division und Modulorechnung Mathematik (Zahlentheorie): Seien a, 0 6= b ∈ Z. Dann existieren eindeutig bestimmte Zahlen q, r ∈ Z mit a = q · b + r, 0 ≤ r < |b| . Computer Science: Seien a, 0 6= b ∈ Z. Dann existieren (eindeutig bestimmte) Zahlen q, r ∈ Z mit a = q · b + r, −|b| < r < |b| . Es gilt: q = a / b; r = a % b; → ganzzahlige Division → a modulo b Beispiele: a = 45 , b = 7: a = −27 , b = 5: 45 = 6 · 7 + 3 45 = 7 · 7 − 4 −27 = (−6) · 5 + 3 −27 = (−5) · 5 − 2 ; ; q=6, q=7, ; ; r = 3 r = −4 q = −6, q = −5, r = 3 r = −2 Operatoren: Implizite Typumwandlung und Casts Unterscheide! int a = -3; float c; c = a / 2; int a = -3; float c; c = a / 2.0; ; c = -1.0 ; c = -1.5 Explizite Typumwandlung durch Casts (Datentyp) Term; Beispiel: int a = 3, b = 2; float c = (float) a / b; ; c = 1.5 Merke: Casts besitzen höhere Priorität als binäre arithmetische Operatoren Äquivalent: float c = ((float) a) / b; Nicht äquivalent: float c = (float) (a / b); Operatoren: Vergleichende und logische Operatoren Vergleichsoperatoren Überprüft werden Wahrheitswerte von Aussagen wie etwa x > 0, j ≤ N, a ≤ f (x ) ≤ b usw. Notation in C a < b a > b a <= b a >= b a == b a != b math. Notation a<b a>b a≤b a≥b a=b a 6= b Häufig verwendet in „wenn, . . . dann . . . “-Konstruktionen in C: Wert 0 (auch 0.0) Werte ungleich 0 (etwa 1, -2.5) ; "falsch" ; "wahr" (false) (true) Unterscheide "a = b" (Zuweisung) und "a == b" (Vergleich) Merke: Der Wert einer Zuweisung entspricht dem zugewiesenen Wert. Vorsicht beim Test auf Gleichheit bei floats (s. arithm. Operatoren) −→ Vergleichsoperatoren sind linksassoziativ (−→ −→) Operatoren: Vergleichende und logische Operatoren Logische Operatoren: && , || , ! Ausdrücke (arithmetische, vergleichende, zuweisende) werden als Aussagen miteinander verknüpft Die Werte solcher Verknüpfungen sind immer 1 (wahr) oder 0 (falsch) Notation in C A && B A || B !A math. Notation A∧B A∨B ¬A Bedeutung Konjunktion (und) Disjunktion (oder) Negation Das "oder" ist einschließend zu verstehen und nicht als "entweder oder" Logische Operatoren sind linksassoziativ (−→) Verknüpfungstafeln: && 0 1 0 0 0 1 0 1 || 0 1 0 0 1 1 1 1 0 1 ! 1 0 Operatoren: Vergleichende und logische Operatoren Beispiel: int A = -4, C = 1; double B = -0.5; Ausdruck A !A A - B !(A - B) !!(A - B) !A - B !A -!B Wert -4 0 -3.5 0 1 0.5 0 Ausdruck A < B < C A < B <= C A < (B<C) A<B && B<C C>A && B A==2 || B>C A=2 || B>C Wert 0 1 1 1 1 0 1 Ausgabe auf der Kommandozeile: printf dient zur Ausgabe von Text und numerischen Werten auf dem Bildschirm erfordert Präprozessordirektive: Syntax: #include <stdio.h> printf(Formatstring, Parameterliste); Formatstring = Text und Platzhalter in Anführungszeichen Beispiel: int a = -5; float b = 3.1415926535; printf("a hat den Wert %d, b hat den Wert %f.\n", a, b); Ausgabe: a hat den Wert -5, b hat den Wert 3.141593. Zeichenkonstanten (vgl. Erlenkötter, Kap. 8.5) \n neue Zeile (new line) \t horizontaler Tabulator Ein- und Ausgabe: Platzhalter %[flags][weite][.genauigkeit][modifizierer]typ typ ganzzahlig unsigned integer Gleitkomma wissenschaftl. d/i u f e modifizierer l, z.B. %lf bei Verwendung von double L, z.B. %Lf bei Verwendung von long double genauigkeit Anzahl der Nachkommastellen weite Mindestanzahl an Zeichen („.“ mitgezählt) flags links- oder rechtsbündig, Vorzeichen, führende Nullen 3218 3218 3218.000000 3.218000e+03 Beispiel: ”%7.3f” ist Platzhalter für eine Gleitkommazahl mit 3 Nachkommastellen und einer Feldbreite von mindestens 7 Zeichen. Hilfe zu printf: Befehl man 3 printf in die Kommandozeile eingeben Weitere Beispiele: Übung Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 26.04.2017 Wiederholung: Ausgabe auf der Kommandozeile: printf dient zur Ausgabe von Text und numerischen Werten auf dem Bildschirm erfordert Präprozessordirektive: Syntax: #include <stdio.h> printf(Formatstring, Parameterliste); Formatstring = Text und Platzhalter in Anführungszeichen Beispiel: int a = -5; float b = 3.1415926535; printf("a hat den Wert %d, b hat den Wert %f.\n", a, b); Ausgabe: a hat den Wert -5, b hat den Wert 3.141593. Zeichenkonstanten (vgl. Erlenkötter, Kap. 8.5) \n neue Zeile (new line) \t horizontaler Tabulator Ein- und Ausgabe: Platzhalter %[flags][weite][.genauigkeit][modifizierer]typ typ ganzzahlig unsigned integer Gleitkomma wissenschaftl. d/i u f e 3218 3218 3218.000000 3.218000e+03 modifizierer l, z.B. %lf bei Verwendung von double L, z.B. %Lf bei Verwendung von long double genauigkeit Anzahl der Nachkommastellen weite Mindestanzahl an Zeichen („.“ mitgezählt) flags links- oder rechtsbündig, Vorzeichen, führende Nullen Beispiel: ”%7.3f” ist Platzhalter für eine Gleitkommazahl mit 3 Nachkommastellen und einer Feldbreite von mindestens 7 Zeichen. Eingabe durch den Benutzer: scanf dient u.a. zum Einlesen von Zahlen erfordert Präprozessordirektive: #include<stdio.h> Syntax: scanf(Formatstring, Parameter); Formatstring: %[modifizierer]typ Parameter: & vor Variablennamen! Beispiel: #include <stdio.h> main() { double zahl; printf("Bitte geben Sie eine Zahl ein: "); scanf("%lf", &zahl); printf("%lf zum Quadrat ist %lf\n", zahl, zahl*zahl); } Zeichen Zeichen (engl. character ) werden intern wie (positive) ganzzahlige Werte behandelt, der Datentyp ist char. Variablen vom Typ char belegen im Speicher 1 Byte. Die standardisierte Zuordnung Zeichen ←→ Zahl erfolgt gemäß der ASCIITabelle. ASCII = American Standard Code for Information Interchange. Die Kodierung definiert 128 Zeichen, bestehend aus 33 nicht-druckbaren sowie 95 druckbaren: 0 Nullzeichen 1–32 Steuerzeichen 33–126 Ziffern, Buchstaben, Symbole, usw. Auszug: 37 38 48 49 % & 0 1 65 66 67 92 A B C \ 97 98 99 123 a b c { 124 167 181 223 Es sind äquivalent char char char char c c c c = = = = 'A'; 65; 0101; // oktal: 65 = 1*64 + 0*8 + 1*1 0x41; // hexadezimal: 65 = 4*16 + 1*1 } $ µ ß Zeichen Ausgabe mittels printf: char c = 88; printf("88 interpretiert als Zeichen: %c\n",c); printf("88 interpretiert als integer: %d\n",c); Zeichenarithmetik char c = 'A'; int diff = 'C' - c; // diff = 2 c = 'B' + diff; // c = 68 printf("Das zu c korrespondierende Zeichen ist %c\n",c); Einlesen von Zeichen via der Funktion getchar int getchar(void) Beispiel int c; c = getchar(); Anweisungsblöcke Folge von Anweisungen, die von geschweiften Klammern eingeschlossen sind Anweisungsblock ist syntaktisch äquivalent zu einer einzelnen Anweisung eine einzelne Anweisung bedarf keiner Klammer Anweisungsblöcke können geschachtelt sein Beispiele: printf("Hallo Welt!\n"); { } { } printf("Hallo Welt!\n"); printf("Ich gehoere zum inneren Anweisungsblock.\n"); printf("Ich bin im uebergeordneten Anweisungsblock!\n"); { printf("Hallo Welt!\n"); printf("Ich gehoere zum inneren Anweisungsblock.\n"); } if-Anweisung if (Bedingung) Anweisungsblock oder if (Bedingung) Anweisungsblock_1 else Anweisungsblock_2 Wenn-Dann Wenn-Dann-Andernfalls Beispiel: Vorzeichenfunktion sign Mathematische Definition: int a; if (a>0) printf("sign(a) = +1\n"); else { if (a==0) printf("sign(a) = 0\n"); else printf("sign(a) = -1\n"); } sign(a) = +1, 0, −1, falls a > 0 falls a = 0 falls a < 0 Mehrfache Alternativen: else if Syntax if (Bedingung_1) Anweisungsblock_1 else if (Bedingung_2) Anweisungsblock_2 .. . else if (Bedingung_N) Anweisungsblock_N else // optional Anweisungsblock // optional Beispiel if (a > 0) printf("sign(a) = +1\n"); else if (a == 0) printf("sign(a) = 0\n"); else // oder else if (a < 0) printf("sign(a) = -1\n"); Viele Alternativen: switch Syntax switch (Variable) { case Wert_1: Anweisungsblock_1 break; // optional case Wert_2: Anweisungsblock_2 .. . case Wert_N: Anweisungsblock_N default: // optional Anweisungsblock // optional } Bei der Ausführung wird zu dem case label gesprungen, an der zum ersten Mal Variable und Wert übereinstimmt. Der gesamte folgende Code bis zum ersten break oder zum Ende des Blocks wird ausgeführt. Das schließt auch Code außerhalb des angesprungenen case labels ein. Als Wert im case label sind nur Konstanten zulässig. Viele Alternativen: switch Beispiel unsigned char eingabe; printf("Bitte Befehl eingeben: "); scanf("%c", &eingabe); switch (eingabe) { case 'q': printf("Programm wird beendet!\n"); break; case 'p': case 'P': printf("Drucken...\n"); break; case 'h': printf("Hilfe wird aufgerufen.\n"); break; default: printf("Eingabe nicht erkannt!\n"); break; } Schleifen Schleifen wiederholen einen Anweisungsblock so lange bis ein bestimmtes Abbruchkriterium erfüllt ist Die wichtigsten Schleifen in C sind for und while while-Schleife oft verwendet, wenn die Anzahl der Wiederholungen nicht vorherbestimmt ist Syntax: while (Bedingung) Anweisungsblock wiederholt Anweisungsblock bis Bedingung "falsch", d.h. gleich 0 ist Autor ist selbst verantwortlich, dass das Abbruchkriterium irgendwann erfüllt ist → Gefahr einer Endlosschleife! Variablen in Bedingung müssen deklariert und ggf. initialisiert werden Anweisungsblock = "Rumpf" der Schleife Beispiel: Programm soll eine natürliche Zahl N einlesen und die Summe 1 + 2 + 3 + . . . + N ausgeben. Beispiel: while-Schleife 1 #include<stdio.h> 2 3 4 5 main() { int i = 1, sum = 0, N; 6 printf("Geben Sie eine natuerliche Zahl ein: N= "); scanf("%d", &N); 7 8 9 while (i <= N) { sum += i; i++; } 10 11 12 13 14 15 16 17 18 } printf("Die Summe der ersten %d natuerlichen Zahlen ", N); printf("betraegt %d\n", sum); Beispiel: while-Schleife Es sind äquivalent: int i = 1; int i = 0; while (i <= N) { sum += i; i++; } while (i < N) { i++; sum += i; } int i = 0; int i = 0; while (i++ < N) sum += i; while (++i <= N) sum += i; ; Kein guter Stil, da fehleranfällig! do-while-Schleife Anweisungsblock wird mindestens ein Mal ausgeführt beachte Semikolon am Ende do Anweisungsblock while (Bedingung); äquivalent zu: Anweisungsblock while (Bedingung) Anweisungsblock Beispiel int N; do { printf("Bitte geben Sie eine ganze Zahl zwischen 5 und 15 ein:"); scanf("%i", &N); } while(N<5 || N>15); for-Schleife vermutlich die am häufigsten verwendete Schleifenvariante kommt zum Einsatz wenn das Update immer gleich ist Anzahl der Wiederholungen ist a priori bekannt Syntax: for (Initialisierung; Bedingung; Update) Anweisungsblock äquivalent zu: Initialisierung; while (Bedingung) { Anweisungsblock Update; } Beispiel: int i; for (i=1; i<=N; i++) sum += i; Stolperfallen Abbruchkriterium fehlerhaft: Zuweisung statt Vergleich for(i=1; i=N; i++) sum += i; → Endlosschleife! Leerer Anweisungsblock: Falsch platziertes Semikolon for(i=1; i<=N; i++); sum += i; Unter- oder Überlauf unsigned i; for(i=N; i>=0; i--) sum += i; unsigned char i; for(i=1; i<=N; i++) sum += i; → Endlosschleife für N>255! Schachtelung Beispiel: 1 #include <stdio.h> 2 3 4 5 main() { int i, j; 6 for(i=1; i<=5; i++) { for(j=1; j<=5; j++) printf("%2d ", i*j); 7 8 9 10 // aeussere Schleife // innere Schleife // Feldbreite 2 -> Zahlen rechtsbuendig 11 12 13 14 } } printf("\n"); Ausgabe: 1 2 3 4 2 4 6 8 3 6 9 12 4 8 12 16 5 10 15 20 5 10 15 20 25 // wieder aussen Steuerung von Wiederholungen break beendet aktuelle Wiederholungsansweisung continue Rest der Schleife wird übersprungen und der nächste Schleifendurchlauf gestartet Merke: break und continue sollten sparsam eingesetzt werden, da sonst das Programm unübersichtlich wird. return beendet aktuelle Funktion (später mehr!) Absolut verpönt: goto bewirkt einen Sprung im Programm an eine zurvor definierte Stelle Merke: Anwendung von goto ist verboten! Steuerung von Wiederholungen Beispiel: 1 #include <stdio.h> 2 3 4 5 main() { unsigned eingabe; 6 while(1) // ohne break eine Endlosschleife! { printf("Bitte eine natuerliche Zahl kleiner als 100"); printf(" eingeben: "); scanf("%u", &eingabe); // Vorsicht: Unterlauf moeglich! 7 8 9 10 11 12 13 14 } 15 if (eingabe < 100) break; 16 17 18 } printf("Die Zahl war %u.\n", eingabe); Zufallszahlen Definition Ein Zufallsexperiment ist ein Experiment, dessen Ausgang nicht aus den vorherigen Ergebnissen vorausgesagt werden kann. Eine Zufallszahl ist eine Zahl, die sich aus dem Ergebnis eines Zufallsexperiments ableitet. „Echte“ Zufallszahlen sind prinzipiell nur solche, die von wirklich zufälligen (physikalischen) Prozessen abgeleitet werden (Beispiel: radioaktiver Zerfall). Viele physikalische Prozesse sind zwar deterministisch (vorhersagbar), jedoch nicht praktisch berechenbar (Beispiel: Temperatur am 9. Dezember des Folgejahres) → Pseudo-Zufall Definition Ein Experiment gilt als pseudo-zufällig, wenn sich sein Ergebnis zwar theoretisch voraussagen lässt, ohne Kenntnis der genauen Berechnungsvorschrift jedoch eine Prognose unmöglich ist. Zufallszahlen Computer kann nur Pseudo-Zufallszahlen erzeugen Ziel (zunächst): Gleichverteilte Pseudo-Zufallszahlen auf [0, 1[ oder auf [0, M[ erzeugt Gute Generatoren müssen eine Reihe von statistischen Tests bestehen Am weitesten verbreitet ist die lineare Kongruenzmethode: 1 Gib einen seed-Wert n0 vor (Benutzereingabe / anderweitige Berechnung) 2 Berechne für feste natürliche Zahlen a, b, M und k = 0, 1, . . .: nk+1 = (a · nk + b) mod M Soll das Ergebnis eine Gleitkommazahl in [0, 1[ sein, berechne xk+1 = nk+1 /M. Die Qualität des Generators hängt entscheidend von den Parametern a, b und M ab! Zufallszahlen Beispiel für einen „schlechten“ Generator (aus Kirsch/Schmitt, Kap. 12): Wähle a = 216 + 3, b = 0, M = 231 Visualisierung: Fasse je 3 aufeinanderfolgende Zahlen zu Vektoren in R3 zusammen. Ergebnis: Jeder Vektor liegt in einer von 15 festen Ebenen! Erzeugung von Zufallszahlen in C Präprozessordirektive #include <stdlib.h> notwendig Für int-Zufallszahlen auf [0, INT_MAX]: Funktion rand() Seed-Wert für den rand-Generator wird mit srand(seed) festgelegt. Dabei wird seed als unsigned int interpretiert. Für double-Zufallszahlen auf [0, 1]: Funktion drand48() Seed-Wert für den drand48-Generator wird mit srand48(seed) festgelegt. Dabei wird seed als long int interpretiert. Verwendung: unsigned seed = 884722; int zz; long seed = 88472250439203531568; double zz; srand(seed); zz = rand(); srand(seed); zz = drand48(); printf("zz = %d\n", zz); printf("zz = %f\n", zz); Beispiel: (primitive) Simulation eines Aktienkurses 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 7 8 9 main() { long seed; int i, periode = 20; // Anzahl Tage double wert, // aktueller Wert max_schwankung = 10; // Tagesschwankung maximal 10 Euro 10 printf("Bitte Seed-Wert eingeben: "); scanf("%ld", &seed); srand48(seed); 11 12 13 14 printf("Wert der Aktie zu Beginn (Euro): "); scanf("%lf", &wert); 15 16 17 for(i=0; i<periode; i++) wert += 2 * (drand48() - 0.49) * max_schwankung; // Skalierung auf [-0.99,1.01] 18 19 20 21 22 23 } printf("Wert nach 20 Tagen: %.2f Euro\n", wert); Monte-Carlo-Methoden Oberbegriff für mathematische Verfahren, deren Funktionsprinzip der Zufall ist Name geht zurück auf John v. Neumann und reflektiert die Tatsache, dass immer wieder „gewürfelt“ wird. Grundlegendes Prinzip ist einfach zu verstehen → bei Anwendern beliebt Trotzdem vielseitig und effizient verwendbar Kommen vor allem dann zum Einsatz, wenn ein zufälliger Prozess simuliert werden soll (z. B. Finanzmathematik, Dynamik von Gasen / Partikeln / Elektronen in Materie, Ausbreitung von Krankheiten, . . .) Mathematische Basis: Gesetz der großen Zahlen (einfache Fassung) Je öfter man ein Zufallsexperiment durchführt, desto mehr nähert sich die relative Häufigkeit eines Ereignisses der Wahrscheinlichkeit desselben Ereignisses an. Beispiel: Idealer Würfel P(X = 1) = 16 – Wahrscheinlichkeit, dass eine Eins gewürfelt wird N1 – Anzahl der gewürfelten Einsen nach N Würfen Gesetz der großen Zahlen: lim N→∞ N1 1 = N 6 Monte-Carlo-Integration |A| = Flächeninhalt von A = ? Z A x dy − y dx A= γ γ Umrandungskurve von A Parametrisierung von γ notwendig Q = [0,1]x[0,1] }h Klassische Vorgehensweise (Quadratur): Unterteile Q in N · N Quadrate der Kantenlänge h = 1/N Bestimme die Anzahl NA der Quadrate, deren Mittelpunkte in A liegen Berechne |A| ≈ NA · h2 Nachteil: sehr teuer in höheren Dimensionen (≥ 3) Monte-Carlo-Integration Stochastischer Zugang: Wahrscheinlichkeit, dass ein zufällig erzeugter Punkt z ∈ Q in A landet, ist gegeben durch |A| pA = P(z ∈ A) = = |A| |Q| Gesetz der großen Zahlen: Für eine hinreichend große Anzahl N von zufällig erzeugten Punkten ist NA #(Punkte in A) = ≈ pA = |A| #(Punkte insgesamt) N Stochastische Vorgehensweise: Erzeuge N zufällige Punkte in Q Zähle die Anzahl NA der Punkte, die in A liegen Berechne |A| = NA N [·|Q|] Vorteile: simpel, einfach zu implementieren man muss nur zwischen z ∈ A und z 6∈ A unterscheiden können in höheren Dimensionen sehr effizient Monte-Carlo-Integration Beispiel: Einheitskreisscheibe im 1. Quadranten 1 Umgebendes Quadrat Q = [0, 1]2 ⇒ |Q| = 1 0.8 Einheitskreis-Viertel A = (x , y ) ∈ Q | x 2 + y 2 ≤ 1 0.6 y Pseudocode: A 0.4 1 Lies eine Zahl N ein 2 Für i = 1, . . . , N: Erzeuge zufällige Zahlen x und y Falls x 2 + y 2 ≤ 1: 0.2 NA ← NA + 1 3 0 0 0.2 0.4 0.6 x 0.8 1 Gib Flächeninhalt = NA /N aus Monte-Carlo-Integration: Ergebnisse 1 1 0.8 0.8 0.6 0.6 0.4 0.4 0.2 0.2 0 0 0 0.2 0.4 0.6 N = 10, Fehler: 2.85 · 0.8 10−1 1 (36.3 %) 0 0.4 0.6 N = 100, Fehler: 4.54 · 1 1 0.8 0.8 0.6 0.6 0.4 0.4 0.2 0.2 0 0.2 0.8 10−2 1 (5.8 %) 0 0 0.2 0.4 0.6 0.8 1 N = 1000, Fehler: 2.26 · 10−2 (2.9 %) 0 0.2 0.4 0.6 0.8 1 N = 10000, Fehler: 4.80 · 10−3 (0.6 %) Monte-Carlo-Simulation: Radioaktiver Zerfall Radioaktive Nuklide (Atomsorten) sind instabil und werden unter Aussendung von Strahlung in andere, stabile Atomsorten umgewandelt. Der Zerfall eines einzelnen Atoms geschieht spontan und kann als zufälliges Ereignis angesehen werden. Das Isotop 131 I (Jod-131) ist ein Betastrahler. Es wird zum stabilen Isotop 131 Xe (Xenon-131) umgewandelt. Dabei wird ein Elektron (Beta-Teilchen) emittiert. Jod-131 besitzt eine Halbwertszeit von T1/2 = 8.02070 Tagen, d. h. nach T1/2 ist (etwa) die Hälfte einer betrachteten Menge Jod-131 zerfallen. Experimente zeigen den Zusammenhang ∆N = −λ · N · ∆t mit ∆t : kleines Zeitintervall N : Anzahl der radioaktiven Kerne ∆N : Änderung von N im Zeitintervall ∆t λ > 0 : Zerfallskonstante (Proportionalitätsfaktor, materialabhängig) Monte-Carlo-Simulation: Radioaktiver Zerfall Klassische Herangehensweise (für große Anzahl N): ∆N(t) = −λN(t)∆t mit ∆N(t) = N(t + ∆t) − N(t). Division durch ∆t und der anschließende Grenzübergang ∆t → 0 liefern die Differentialgleichung Ṅ(t) = −λN(t). Die (eindeutige) Lösung ist gegeben durch N(t) = N(0) e −λt . Die Halbwertszeit T1/2 ist der Zeitraum, nach dem die Hälfte der Kerne zerfallen ist, d. h. N(T1/2 ) = N(0)/2. Durch eine einfache Umformung ergibt sich T1/2 = ln 2 λ bzw. λ= ln 2 . T1/2 Halbwertszeiten radioaktiver Isotope sind üblicherweise tabelliert. Stochastische Herangehensweise: λ∆t = Wahrscheinlichkeit, dass ein Kern im Zeitraum ∆t zerfällt In jedem Zeitschritt wird für jeden Kern zufällig entschieden, ob er zerfällt. Monte-Carlo-Simulation: Radioaktiver Zerfall Beispiel: Jod-131 Als Zeitschritt wählen wir ∆t = 1 min. T1/2 = 8.0207 d = 11549.808 min ⇒ λ= ln 2 = 6.001 · 10−5 min−1 T1/2 Die Wahrscheinlichkeit, dass ein Kern innerhalb einer Minute zerfällt, ist gegeben durch p = λ∆t = 6.001 · 10−5 . Pseudocode: 1 Lies eine Zahl N (Anzahl zu Beginn) und eine Zahl T (Endzeitpunkt) ein 2 Für i = 1, . . . , T : Für j = 1, . . . , N: • Erzeuge eine zufällige Zahl x ∈ [0, 1[ • Falls x < p, setze N ← N − 1 3 Gib die Anzahl N der übriggebliebenen Kerne nach T Minuten aus Monte-Carlo-Simulation: Radioaktiver Zerfall – Ergebnisse 1 1 N(0)=10 N(0)=100 N(0)=10000 Analytisch 0.8 Vorhandene Kerne / Anfangsbestand Vorhandene Kerne / Anfangsbestand 0.8 Realisierung 1 Realisierung 2 Realisierung 3 0.6 0.4 0.2 0.6 0.4 0.2 0 0 2000 4000 6000 Zeit [min] 8000 10000 Zerfallskurven für verschiedene Anfangsbestände N(0) 0 0 2000 4000 6000 Zeit [min] 8000 10000 3 Realisierungen für N(0) = 10 Monte-Carlo-Simulation: Räuber-Beute-Modell Grundlegendes Modell in der Theorie der dynamischen Systeme Beschreibt die Zusammenhänge, welche die Entwicklung mehrerer interagierender Populationen (konkurrierende Spezies) bestimmen Anwendungsgebiete: Systembiologie, Epidemologie, Ökonomie, . . . Beispiel: Hecht vs. Karpfen Räuber Beute sehr gefräßig harmlos mag Karpfen lecker Monte-Carlo-Simulation: Räuber-Beute-Modell Grundannahmen: In einem großen Weiher leben ausschließlich Karpfen (K ) und Hechte (H). Der Nahrungsvorrat für Karpfen ist unbegrenzt. Ihr Wachstum in einem Zeitintervall ∆t ist proportional zur Anzahl der vorhandenen Individuen: ∆K ∼ K ∆t Hechte, die keine Nahrung finden, fressen andere Hechte, d. h. der Rückgang ihres Bestandes in einem Zeitintervall ∆t ist proportional zur Zahl der vorhandenen Exemplare: ∆H ∼ −H∆t ; Ohne Interaktion gilt für beide Spezies das gleiche Modell wie beim radioaktiven Zerfall! Bei einer Begegnung wird der Karpfen vom Hecht gefressen. Hat ein Hecht eine bestimmte Anzahl Karpfen gefressen, „entsteht“ ein zusätzlicher Hecht. Je mehr Individuen es von beiden Sorten gibt, desto wahrscheinlicher ist solch eine Begegnung: ∆K ∼ −KH∆t ∆H ∼ KH∆t Monte-Carlo-Simulation: Räuber-Beute-Modell Gekoppeltes Gesamtmodell (∆t = 1): ∆K = λK K − p KH ∆H = n−1 p KH − λH H ∆K /H : Änderung des Bestandes von K /H λK : Zuwachsrate K λH : Sterberate H p : Wahrscheinlichkeit einer Begegnung n : Anzahl der K , die ein H zur Reproduktion fressen muss Parameterwahlen für die Simulation: λK = 2 · 10−3 , −6 p = 5 · 10 λH = 5 · 10−4 , , n = 5. Aus dem kontinuierlichen Modell bekannt: Ein stabiler Gleichgewichtszustand ist gegeben durch K∗ = nλH = 500, p H∗ = λK = 400. p Monte-Carlo-Simulation: Räuber-Beute-Modell Pseudocode: 1. Lies die Anfangsbestände K und H sowie die Anzahl T der Zeitschritte ein 2. Für i = 1, . . . , T : Für k = 1, . . . K : • Erzeuge eine zufällige Zahl x ∈ [0, 1[ • Falls x < λK : Setze K ← K + 1 Für k = 1, . . . , K : Für j = 1, . . . , H: • Erzeuge eine zufällige Zahl x ∈ [0, 1[ • Falls x < p: Setze K ← K − 1 Setze ntemp ← ntemp + 1 Falls ntemp = n: Setze H ← H + 1 und ntemp ← 0 break; Für j = 1, . . . , H: • Erzeuge eine zufällige Zahl x ∈ [0, 1[ • Falls x < λH : Setze H ← H − 1 Gib die Zahlen K und H am Bildschirm aus Monte-Carlo-Simulation: Räuber-Beute-Modell – Ergebnisse 650 900 Karpfen Hechte Karpfen Hechte 800 600 700 Population Population 550 500 600 500 450 400 400 300 350 200 0 2000 4000 6000 8000 10000 12000 Zeitschritte 14000 16000 18000 20000 0 2000 K = 500, H = 400 2000 6000 8000 10000 12000 Zeitschritte 14000 16000 18000 20000 K = 800, H = 400 4000 Karpfen Hechte 1800 4000 Karpfen Hechte 3500 1600 3000 1200 Population Population 1400 1000 800 2500 2000 1500 600 1000 400 500 200 0 0 0 2000 4000 6000 8000 10000 12000 Zeitschritte 14000 K = 900, H = 800 16000 18000 20000 0 2000 4000 6000 8000 10000 12000 Zeitschritte 14000 K = 800, H = 1000 16000 18000 20000 Monte-Carlo-Simulation: Räuber-Beute-Modell – Ergebnisse 1400 K=500, K=800, K=900, K=800, 1200 H=400 H=400 H=800 H=1000 Hechte 1000 800 600 400 200 0 0 500 1000 1500 2000 Karpfen 2500 3000 Phasenraumdiagramm für verschiedene Startwerte 3500 4000 Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 03.05.2017 Funktionen Funktion ≈ Zusammenfassung eines Anweisungsblocks zu einer aufrufbaren Einheit Gehört zu den wichtigsten Konzepten fast aller Programmiersprachen Aufgaben von Funktionen: 1 2 3 4 Wiederverwendbarkeit von einmal geschriebenem Code Strukturierung und Vereinfachung von Code ; bessere Übersicht und Lesbarkeit einfachere Fehlersuche, separates Testen möglich leichteres Hinzufügen weiterer Funktionalitäten ; Die sinnvolle Strukturierung eines Programms in Unterprogramme ist einer der wichtigsten Schritte bei der Programmierung! Charakterisierung von Funktionen Eine Funktion besitzt einen sinnvollen (=sprechenden) Namen, mit dem sie aufgerufen wird. nimmt eine (möglicherweise leere) Liste von Parametern mit festgelegten Datentypen als Eingabe. hat einen Anweisungsblock, der bei ihrem Aufruf ausgeführt wird. liefert nichts oder einen Wert eines festgelegten Datentyps als Ausgabe „Mathematische“ Schreibweise: Funktion : {int, float, ...}N −→ {int, float, ...} oder {}, N ≥ 0 (Parameter1, . . . , ParameterN) 7−→ Ausgabewert Anweisungen Im Gegensatz zu mathematischen Funktionen kann der Anweisungsblock auch Befehle enthalten, die nicht direkt etwas mit der Ausgabe zu tun haben (auch „Funktion: {} → {}“ kann in C sinnvoll sein). Syntax Deklaration Rückgabetyp Funktionsname(ParTyp1 [Par1], ..., ParTypN [ParN]); Definition Rückgabetyp Funktionsname(ParTyp1 Par1, ..., ParTypN ParN) { Anweisungsblock } Unterschied zwischen Deklaration und Definition: verschiedene Abstraktionsebenen! Deklaration (= Signatur) legt fest, was eine Funktion tut. muss vor dem erstmaligen Aufruf und außerhalb von main im Code stehen. Definition legt fest, wie eine Funktion etwas tut. kann an einer beliebigen Stelle außerhalb von main im Code stehen. endet mit einem Semikolon. hat kein Semikolon am Ende. Parameter-Datentypen genügen. Parameter sind mit Typ und Name anzugeben. Beispiel 1 2 /** Praeprozessordirektive ****/ #include <stdio.h> 3 4 5 6 /** Funktionsdeklarationen ****/ float summe(float a, float b); 7 // auch moeglich: // float summe(float, float); 8 9 10 11 12 13 14 15 /** Hauptprogramm ****/ int main(void) { float sum; sum = summe(3.5, 1); } // Funktionsaufruf 16 17 18 19 20 21 22 23 /** Funktionsdefinitionen ****/ float summe(float a, float b) { float sum = a + b; return sum; } // Funktionsrumpf // Funktionsrumpf Der Datentyp void Funktionen ohne Rückgabe: Deklaration: void Funktion(Parameterliste); Beispiel: void srand48(long seed); Verwendung: z. B. Bildschirmausgabe, Statusänderung eines externen „Mechanismus“ In anderen Programmiersprachen (z.B. Pascal) oft als Prozedur bezeichnet. In C gibt es keine Differenzierung zwischen Funktion und Prozedur. Funktionen ohne Parameter: Deklaration: Rückgabetyp Funktion(void); Beispiel: double drand48 (void); Verwendung: z. B. Ausführung von externen „Mechanismen“ Kombination: z. B. void abort(void); (Gewaltsames Ende des Programms) return und main return Mit der Ausführung der return-Anweisung wird die aktuelle Funktion sofort beendet. Enthält der Funktionsrumpf keine return-Anweisung, so endet die Ausführung des Rumpfes bei Erreichen der letzten schließenden geschweiften Klammer. → schlechter Stil! Beachte: Es kann immer nur ein (skalarer) Wert zurückgegeben werden. main main ist in Wirklichkeit eine Funktion (mit Rückgabetyp int). Quelltext innerhalb von main wird als Hauptprogramm bezeichnet. Es sind äquivalent: main() int main() int main(void) main liefert standardmäßig den Rückgabewert 0 wenn keine Fehler aufgetreten sind. Mit return . . .; können andere Werte zurückgeliefert werden. → Fehlerbehandlung Beispiel 1: Funktion mit Rückgabewert Die charakteristische Funktion eines Intervalls [a, b] mit a < b ist definiert als χ[a,b] (x ) := 1 0 , , x ∈ [a, b] sonst. Implementierung: je nach Wert von x wird 1.0 oder 0.0 zurückgegeben. Definition im Code: // Charakteristische Funktion des Intervalls [a,b] // (1 innerhalb, 0 ausserhalb) float charFunkIntervall(const float a, const float b, float x) { if(a >= b) // Hier muss eine Fehlerbehandlung hin } if ((x >= a) && (x <= b)) return 1.0; else return 0.0; Beispiel 2: Funktion ohne Rückgabewert void geplapper(int zahl1, double zahl2, char c) { printf("Diese Funktion erzeugt eine Menge (sinnloser) Ausgaben "); printf("am Bildschirm.\n\n"); printf("Jetzt noch eine horizontale Linie, dann geht's los!\n"); printf("---------------------------------------------------\n"); printf("Zahl1 = %d, Zahl2 = %f\n", zahl1, zahl2); if(c == '+') printf("Die Summe der beiden Zahlen ist %f\n\n", zahl1+zahl2); printf("So, jetzt bin ich fertig!\n"); } return; Mathematische Funktionen Nutzung erfordert Präprozessordirektive #include <math.h> Compilierung gcc ProgrammName.c -o ProgrammName -lm Signatur int abs(int a) Bedeutung |a| float fabsf(float a) |a| double fabs(double a) |a| √ x double sqrt(double x) double pow(double b, double e) be double exp(double x) ex double log(double x) double log10(double x) ln(x ) log10 (x ) Mathematische Funktionen Signatur double sin(double x) Bedeutung sin(x ) double cos(double x) cos(x ) double tan(double x) tan(x ) double asin(double x) arcsin(x ) double acos(double x) arccos(x ) double atan(double q) arctan(q) ∈ (− π2 , double atan2(double x, double y) arctan(y /x ) ∈ (−π, π] double sinh(double x) sinh(x ) double cosh(double x) cosh(x ) double floor(double x) bx c double ceil(double x) dx e π ) 2 Konstanten in math.h Name M_E Bedeutung e M_LOG2E log2 (e) M_LOG10E log10 (e) M_LN2 ln(2) M_LN10 ln(10) M_PI π M_PI_2 π/2 M_PI_4 π/4 M_1_PI 1/π √ 2 √ 1/ 2 M_SQRT2 M_SQRT1_2 Bemerkung: Die Namen der Konstanten werden vom Präprozessor im Code textuell durch die entsprechenden Werte ersetzt, z. B. M_PI durch 3.14159265358979323846. (Gleitkommazahlen werden automatisch als double interpretiert.) Call by Value Beispiel: Vertauschen zweier Werte (?) 1 #include <stdio.h> 2 3 4 // Funktionsdeklaration void vertausche(int p, int q); // oder (int, int) 5 6 7 8 9 10 11 12 13 14 // Hauptprogramm int main(void) { int a = 1, b = 3; vertausche(a, b); printf("a = %d, b = %d\n", a, b); return 0; // guter Stil } 15 16 17 18 19 20 21 22 23 // Funktionsdefinition void vertausche(int p, int q) { int hilf = p; p = q; q = hilf; return; } Ausgabe: a = 1, b = 3 Die Werte wurden gar nicht vertauscht! Warum? Call by value Definition Bei einem Funktionsaufruf werden nicht die Variablen als solche, sondern lediglich ihre Werte, d.h. Kopien der Variableninhalte übergeben. + Funktionsaufrufe können direkt als Parameter für eine andere Funktion verwendet werden, da der Wert und nicht die Funktion selbst übergeben wird. Beispiel: printf("Wurzel von 2 = %f\n", sqrt(2.0)); + Unbeabsichtigte Manipulation der Variablen durch Funktionen wird vermieden. – Der Manipulation von Variablen sind Grenzen gesetzt, da immer nur ein (skalarer) Wert zurückgeliefert werden kann. (Ausweg: Zeiger und Call by reference, später) Häufige Fehlerquelle: Annahme, dass Funktionsparameter durch die Funktion verändert werden können. Dem ist nicht so! Beispiel: float a = 2.0; sqrt(a); Hier wird der Rückgabewert von sqrt mit Parameter 2.0 nicht wieder in a gespeichert, sondern verworfen! Scope und Lifetime Definition (Scope) Der Scope (Sichtbarkeit) eines deklarierten Objekts (Variable oder Funktion) ist der Bereich im Quelltext, in dem es bekannt, d. h. mit seinem Namen aufrufbar ist. Generell gilt: Variablen sind innerhalb des textuellen Codeblocks sichtbar, in dem sie deklariert wurden. Funktionen sind ab ihrer Deklaration in der gesamten Datei sichtbar. Definition (Lifetime) Die Lifetime (Lebenszeit) einer Variablen beschreibt den Zeitraum, in dem der Speicherbereich der Variablen für sie reserviert ist. Grundregel: Eine Variable existiert vom Moment ihrer Deklaration bis zu dem Zeitpunkt, an dem der Block, welcher die Deklaration umschließt, verlassen wird. Lokale und globale Variablen Lokale Variablen Lokale Variablen werden zu Beginn eines Anweisungsblocks deklariert. Lifetime: Bis zum Ende des Anweisungsblocks, also auch in inneren Blöcken Scope: Innerhalb des Blocks, sofern sie nicht durch Variablen gleichen Namens in untergeordneten Blöcken überdeckt werden Funktionen liegen textuell außerhalb jedes anderen Blocks → lokale Variablen sind dort generell nicht sichtbar. Globale Variablen Globale Variablen werden außerhalb aller Funktionen (einschl. main) deklariert. Namenskonvention: Unterstrich am Ende des Namens, z. B. int var_ = 42; Lifetime: Gesamte Dauer der Programmausführung (auch über Dateigrenzen hinweg → später) Scope: Überall (auch in Funktionen) Gefahren: Namenskonflikte, unkontrollierte Manipulation, Chaos → Nutzung globaler Variablen auf ein Minimum reduzieren! Sichtbarkeit: Beispiel 1 1 #include <stdio.h> 2 3 4 5 6 7 8 9 10 int main(void) { int a = 4; { int a = 5; printf("Innen: a = %d\n", a); } printf("Aussen: a = %d\n", a); 11 12 13 } return 0; Ausgabe: Innen: a = 5 Aussen: a = 4 Sichtbarkeit: Beispiel 2 1 #include <stdio.h> 2 3 4 5 int main(void) { int i; 6 for (i=5; i<10; i++) { int i=0; i++; printf("In Schleife: i = %2d\n", i); } 7 8 9 10 11 12 13 14 15 16 } printf("Nach Schleife: i = %2d\n", i); return 0; Ausgabe: In Schleife: In Schleife: In Schleife: In Schleife: In Schleife: Nach Schleife: i i i i i i = 1 = 1 = 1 = 1 = 1 = 10 Lokale und globale Variablen 1 #include <stdio.h> 2 3 int a_ = 10; // globale Variable 4 5 6 int funktion(int); void prozedur(void); 7 8 9 10 11 12 13 14 15 int main(void) { prozedur(); prozedur(); funktion(a_); printf("a_ = %d\n", a_); return 0; } 16 17 18 19 20 21 void prozedur(void) { a_ *= a_; return; } 22 23 24 25 26 int funktion(int a_) { return(--a_); } Ausgabe: a_ = 10000 // schlechter Stil: nur globale Variablen // sollten mit "_" enden! Automatische und statische Variablen Bisher waren alle Variablen automatische Variablen, d.h. sie existieren bis zu dem Zeitpunkt, an dem der Block, welcher die Deklaration umschließt, verlassen wird. Statische Variablen Möglichkeit, dass eine Funktion beim nächsten Durchlauf die Information, die in der Variablen gespeichert wurde, verwenden kann (wie in einem Gedächtnis) Sichtbarkeit: Innerhalb des Blocks, sofern sie nicht durch Variablen gleichen Namens in untergeordneten Blöcken überdeckt werden Deklaration: static Datentyp Name=Wert; Beispiel: 1 2 3 4 5 void zaehle() { static int i = 1; // i wird (nur) beim ersten Aufruf von zaehle initialisiert printf("%d\n", i); i = i + 1; } Rekursive Programmierung Rekursion Aufruf einer Funktion durch sich selbst. Iteration Wiederholung eines Anweisungsblocks. Bemerkungen zur rekursiven Programmierung: Man muss die Schachtelungstiefe der Rekursion selbst überwachen, sonst kann es zum sog. Stapelüberlauf (engl. stack overflow ) kommen. Meist sind Iteration und Rekursion äquivalent, aber häufig ist die Überführung in die jeweils andere Variante nicht offensichtlich. Je nach Fragestellung (Laufzeit-, Speicher-, Lesbarkeitsoptimierung) entscheidet man sich für eine der beiden Methoden. Viele effiziente Sortieralgorithmen oder Divide-and-Conquer -Techniken basieren auf dem Prinzip der Rekursion. Rekursive Programmierung Einfaches Beispiel: Fakultät n! = n(n − 1) · · · 1 = fac(n) Es gilt: fac(n) = n * fac(n-1) 1 ,n > 1 , n ∈ {0, 1} Interessanteres Beispiel: Quersumme einer natürlichen Zahl Algorithmus 1 Die Quersumme einer einstelligen Zahl ist die Zahl selbst. 2 Die Quersumme einer mehrstelligen Zahl ist die Summe der letzten Ziffer und der Quersumme der Zahl ohne ihre letzte Ziffer. Beispiel: Quersumme(5) = 5 Quersumme (31415) = Quersumme (3141) + 5 Quersumme iterativ 1 #include <stdio.h> 2 3 4 5 int main(void) { int zahl, qsumme = 0; 6 printf("Zahl = "); scanf("%d", &zahl); 7 8 9 printf("Quersumme(%d) = ", zahl); 10 11 while (zahl) { qsumme += zahl % 10; zahl /= 10; } 12 13 14 15 16 17 printf("%d\n", qsumme); 18 19 20 21 } return 0; Quersumme rekursiv 1 #include <stdio.h> 2 3 int qsumme(int); 4 5 6 7 8 9 int main(void) { int zahl; printf("Zahl = "); scanf("%d", &zahl); 10 printf("\nQuersumme(%d) = %d\n",zahl, qsumme(zahl)); 11 12 13 14 } return 0; 15 16 17 18 19 20 int qsumme(int zahl) { if (zahl / 10) return zahl % 10 + qsumme(zahl / 10); 21 22 23 } return zahl; Funktionen – elementare Merkregeln Deklaration und Definition Die Funktionsdeklaration steht im Code vor main. Die Funktionsdefinition kommt ans Ende der Datei (unauffällig „versteckt“). Anzahl und Datentypen der Parameter sowie Rückgabetyp müssen übereinstimmen. Guter Stil: Deklaration mit Kommentar versehen, der Parameter und Rückgabewert erläutert Der Name soll die Tätigkeit (Rückgabetyp void) bzw. den zurückgegebenen Wert (nichtleerer Rückgabetyp) widerspiegeln. Jede Funktion sollte durch ein return [Wert]; beendet werden. Aufruf Die Anzahl der Parameter muss konsistent mit der Deklaration sein. Es findet Call by value statt, d. h. die Funktion hat nicht die Parameter selbst, sondern nur die darin gespeicherten Werte zur Verfügung. Daher können diese auch nicht permanent verändert werden! Die übergebenen Werte werden automatisch in die Datentypen laut Deklaration umgewandelt („gecastet“). Bei inkompatiblen Typen warnt der Compiler lediglich. Funktionen „sehen“ nur globale Variablen, übergebene Parameter sowie lokale Variablen im Funktionsrumpf. Statische Felder Definition Ein Feld (engl. array ) ist die Zusammenfassung von Elementen gleichen Typs zu einer aufrufbaren Einheit. Deklaration: Datentyp Feldname[Anzahl]; Legt ein Feld von Anzahl Elementen des Typs Datentyp an. Achtung: Anzahl muss ein positiver ganzzahliger Wert sein. Wie bei primitiven Datentypen ist eine Initialisierung bei der Deklaration möglich: int N[4] = {1, 3, -5, 42}; double x[] = {1.9, -3.1415, 5.73e+21}; // Die Groesse (3) wird hier vom Compiler automatisch bestimmt int p = 23; unsigned j[p] = {1, 0, 3}; // Wichtig: p initialisieren! // Rest bleibt uninitialisiert float y[2] = {1.0, 3, -7.2}; // Fehler: zu viele Elemente! Statische Felder Die Deklaration float x[5]; legt ein Feld der Länge 5 an. Die Komponenten (Feldeinträge, Feldelemente) sind dabei alle vom Typ float. Folgerung: Das Feld mit Bezeichner x belegt im Arbeitsspeicher 5 · 4 = 20 Byte. Auf die Komponenten kann mittels x[0], x[1], x[2], x[3] und x[4] zugegriffen werden. Beispiel: x[0] x[1] x[2] x[3] x[4] = = = = = 11.0; 12.0; 13.0; 14.0; 15.0; Merke: Die Indizierung von Feldeinträgen beginnt in C stets mit 0! 1. Komp. 4 Byte x[0] 11.0 2. Komp. 4 Byte x[1] 12.0 3. Komp. 4 Byte x[2] 13.0 4. Komp. 4 Byte x[3] 14.0 5. Komp. 4 Byte x[4] 15.0 ← Größe ← "Name" ← Inhalt Merke: Alle Komponenten eines Feldes werden vom Compiler direkt hintereinander im Arbeitsspeicher abgelegt. Einschränkungen und Stolperfallen Einmal festgelegt, kann die Größe eines statischen Feldes nicht mehr verändert werden. Der maximalen Größe eines Feldes sind enge Grenzen gesetzt (Gefahr eines stack overflow = Stapelüberlauf). Die Rückgabe eines Feldes durch eine Funktion oder die Übergabe eines Feldes als Parameter einer Funktion ist nicht möglich, da es sich nicht um einen primitiven, sondern einen zusammengesetzten Datentyp handelt. Beim Zugriff auf ein Feldelement außerhalb des zulässigen Indexbereichs erfolgt im allgemeinen keine Fehlermeldung! Auch der Compiler warnt nicht! Die Zuweisung x[7] = 241.98 schreibt in den Bereich, der (zufällig) von alpha belegt wird. Mögliche Folge: Das Programm wird völlig unberechenbar! Häufigste Fehlerquelle: int x[N]; ... x[N] = 1; Tritt zumeist dann auf, wenn x[1] statt x[0] als erster Eintrag interpretiert wird. Beispiel: Euklidische Norm eines Vektors im R3 1 2 #include <stdio.h> #include <math.h> 3 4 5 6 7 int main(void) { double x[3], norm2 = 0.0, norm; int i; 8 // Vektor einlesen for (i=0; i<3; i++) // Vorsicht: Indices beginnen bei 0! { printf("Geben Sie die %d-te Komponente ein: ", i+1); scanf("%lf", &x[i]); } 9 10 11 12 13 14 15 // Vektor ausgeben printf("\nDer Vektor hat folgende Eintraege:\n"); for (i=0; i<3; i++) printf("x[%d] = % 7.4lf\n", i, x[i]); 16 17 18 19 20 // Berechnung der Summe der Komponentenquadrate for (i=0; i<3; i++) norm2 += x[i]*x[i]; 21 22 23 24 norm = sqrt(norm2); 25 // euklidische Norm 26 27 28 29 } printf("\nDie Norm des Vektors ist % .4lf.\n\n", norm); return 0; Beispiel: Euklidische Norm eines Vektors im R3 Ausgabe: Geben Sie die 1-te Komponente ein: 1 Geben Sie die 2-te Komponente ein: -2 Geben Sie die 3-te Komponente ein: 2 Der Vektor hat folgende Eintraege: x[0] = 1.0000 x[1] = -2.0000 x[2] = 2.0000 Die Norm des Vektors ist 3.0000. Mehrdimensionale Felder Deklaration: Datentyp Feldname[dim1][dim2]...[dimN]; Beachte: A[4][3] wird (unabhängig vom Typ) im Speicher „zeilenweise“ abgelegt: A[0][0], A[0][1], A[0][2], A[1][0], . . . , A[1][2], A[2][0], . . . , A[3][2] Mathematische Interpretation: A = (aij ) 1≤i≤4 ∈ R4×3 1≤j≤3 A[0][0] A[1][0] A[2][0] A[3][0] A[0][1] A[1][1] A[2][1] A[3][1] A[0][2] a11 A[1][2] a21 ←→ A[2][2] a31 A[3][2] a41 a12 a22 a32 a42 a13 a23 a33 a43 2D-Felder als Matrizen Beispiel: Zeilenweises Einlesen der Komponenten einer 2 × 3-Matrix vom Typ int 1 #include <stdio.h> 2 3 4 5 6 int main(void) { int i, j; int A[2][3]; 7 for(i=0; i<2; i++) { for(j=0; j<3; j++) { printf("A[%d][%d] = ", i, j); scanf("%d", &A[i][j]); } // for j } // for i 8 9 10 11 12 13 14 15 16 17 18 } return 0; 2D-Felder als Matrizen Auch bei mehrdimensionalen Feldern ist eine direkte Initialisierung möglich, bspw. int A[3][2] = {{11,12},{21,22},{31,32}}; Da die Komponenten im Speicher in einer Reihe angeordnet sind, ist obige Zeile äquivalent zu int A[3][2] = {11,12,21,22,31,32}; Erfolgt bei der Deklaration eine partielle Initialisierung, so wird mit Nullen aufgefüllt: int A[3][3] = {{1,2},{3},{4,5}}; generiert die Matrix " A= 1 3 4 2 0 5 0 0 0 # . Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 10.05.2017 Zeichenketten Zeichenketten (engl. strings) sind formal nichts anderes als Felder vom Typ char. Eine Zeichenkette ("Hallo Welt!\n") wird vom Compiler automatisch als Feld von Zeichen dargestellt. Dabei wird am Schluss automatisch ein zusätzliches Zeichen ’\0’ (Nullzeichen) angehängt, um das Stringende zu markieren. Stringverarbeitungsfunktionen benötigen unbedingt das Nullzeichen, damit sie das Ende eines Strings erkennen. Somit muss bei der Deklaration ein zusätzlicher Speicherplatz für ’\0’ eingeplant werden. Beispiel: char wort[6] = "Hallo"; 0 H 1 a 2 l 3 l 4 o 5 \0 printf("Der 2. Buchstabe von wort ist ein '%c'.\n", wort[1]); Ausgabe: Der 2. Buchstabe von wort ist ein ’a’. Zeichenketten Direkte Initialisierung: als String char wort[] = "Hallo"; oder als Feld von Zeichen char wort[] = {'H', 'a', 'l', 'l', 'o', '\0'}; Einlesen von Zeichenketten mittels fgets: fgets(Zielstring, Anzahl + 1, stdin); liest Zeichen von der Tastatur ein, bis ein ’Return’ eingegeben wird, und speichert die ersten Anzahl Zeichen und ein abschließendes ’\0’ im Zielstring. Bemerkung: Die „einfachere“ Funktion gets prüft nicht die Länge des Zielstrings. Daraus ergeben sich u. U. gravierende Sicherheitsmängel (Gefahr eines Überlaufs), weshalb die Funktion unter Programmierern „geächtet“ ist. Ausgabe via printf wie üblich. Der Platzhalter ist %s. printf("%s\n", wort); Operationen mit Strings (kleine Auswahl) #include <string.h> strlen(s) liefert Länge von s, abschließendes ’\0’ nicht mitgezählt strncpy(s, t, n) kopiert höchstens n Zeichen von t nach s (*) strncat(s, t, n) hängt n Zeichen von t an das Ende von s an (*) ( strcmp(s, t) >0 <0 0 , , , falls s lexikographisch kleiner als t ist falls s lexikographisch größer als t ist falls s und t identisch sind Beachte: if (strcmp(s, t)) testet auf Ungleichheit! strncmp(s, t, n) wie strcmp, aber nur für die ersten n Zeichen (*) Vorsicht: Der Programmierer ist dafür verantwortlich, dass in s genügend Platz vorhanden ist, um n Zeichen zu speichern. Das sollte immer geprüft werden! Operationen mit Strings: Beispiel 1 2 #include <stdio.h> #include <string.h> 3 4 5 6 int main(void) { char str1[] = "Modellierung", vl_eigenschaft[8], zu_kurz[3], vl_name[51]; 7 strncpy(vl_name, str1, strlen(str1)); 8 // okay 9 // simpler Test auf ausreichende Laenge if (50 >= strlen(vl_name) + strlen(" und ") + strlen("Programmierung")) { strncat(vl_name, " und ", 5); strncat(vl_name, "Programmierung", strlen("Programmierung")); } 10 11 12 13 14 15 16 printf("Wie findest du %s? ", vl_name); fgets(vl_eigenschaft, 8, stdin); 17 18 19 printf("\n%s ist deiner Meinung nach %s\n", vl_name, vl_eigenschaft); 20 21 strncpy(zu_kurz, str1, strlen(str1)); // Ueberlauf! printf("Inhalt von zu_kurz: %s\n", zu_kurz); printf("Inhalt von vl_eigenschaft: %s\n", vl_eigenschaft); 22 23 24 25 26 27 } return 0; Operationen mit Strings: Beispiel Ausgabe: Wie findest du Modellierung und Programmierung? okay Modellierung und Programmierung ist deiner Meinung nach okay Inhalt von zu_kurz: Modellierungodellierung Inhalt von vl_eigenschaft: ellierungodellierung (Die Ausgabe der letzten beiden Zeilen hängt vom Speicherlayout ab.) Vorsicht: Offenbar warnt der Compiler nicht vor dem Überlauf in Zeile 22, und auch zur Laufzeit tritt kein Fehler auf. Die Funktion strncpy schreibt über die Feldgrenzen von zu_kurz hinaus in einen Bereich, der (möglicherweise) von einer anderen Variable belegt wird. Da ein String erst mit dem Nullzeichen als beendet gilt, wird von printf bei der Ausgabe des fehlerhaften Strings aus einem fremden Bereich gelesen! Ergebnis: Ein unberechenbares Programm mit einem äußerst schwierig zu lokalisierenden Fehler. Umwandlung von Strings #include <stdlib.h> String −→ Ganzzahl: int atoi(Zeichenkette) int n; char s[11]; printf("Es ist atoi(\"101\") = %d \n", atoi("101")); n = atoi("3218"); printf("Es ist atoi(\"3218\") = %d \n", n); strncpy(s, "-157", 10); printf("Es ist atoi(s) = %d \n", atoi(s)); Ausgabe: Es ist atoi("101") = 101 Es ist atoi("3218") = 3218 Es ist atoi(s) = -157 Umwandlung von Strings #include <stdlib.h> String −→ Gleitkommazahl: double atof(Zeichenkette) double x; char s[11]; printf("Es ist atof("101.32") = %lf \n", atof("101.32")); x = atof("3218.927"); printf("Es ist atof("3218.927") = %lf \n", x); strncpy(s, "-157.58", 10); printf("Es ist atof(s) = %lf \n", atof(s)); Ausgabe: Es ist atof("101.32") = 101.320000 Es ist atof("3218.927") = 3218.927000 Es ist atof(s) = -157.580000 Größe von Datentypen und Speicherobjekten: sizeof Allgemein #include <stdlib.h> Rückgabetyp von sizeof ist size_t. Dabei gilt: size_t ist ganzzahlig und vorzeichenlos, entspricht unsigned (int) oder unsigned long. sizeof gibt den Speicherbedarf eines Datentyps in Byte aus: size_t sizeof(Datentyp); printf("sizeof(char) = %u, ",sizeof(char)); printf("sizeof(int) = %u\n",sizeof(int)); printf("sizeof(float) = %u, ",sizeof(float)); printf("sizeof(double) = %u\n",sizeof(double)); Ausgabe: sizeof(char) = 1, sizeof(int) = 4 sizeof(float) = 4, sizeof(double) = 8 Größe von Datentypen und Speicherobjekten: sizeof sizeof liefert den Speicherbedarf eines deklarierten Speicherobjekts in Byte: size_t sizeof Speicherobjekt; oder size_t sizeof(Speicherobjekt); Beispiel int i; int j=42; float x; float y=3.1415; double z=M_PI; char s[10]; char t[]= "Ay, caramba!"; int a[6]; int b[]={3,2,1,8}; float c[]={1.1,2.2,3.3}; double A[3][2]; int B[][] = {{11,12,13},{21,22,23}}; sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof i j x y z s t a b c A B ; ; ; ; ; ; ; ; ; ; ; ; 4 4 4 4 8 10 13 24 16 12 48 error Speicheradressen Adress-Operator & Erinnerung: int a; scanf("%d",&a); Beispiel: Adressen von Skalaren: int i=1, j=9; double x=2.7, y=M_PI; printf("Adresse von i = %p\t",&i); printf("Wert von i = %d\n", i); printf("Adresse von j = %p\t",&j); printf("Wert von j = %d\n", j); printf("Adresse von x = %p\t",&x); printf("Wert von x = %lf\n", x); printf("Adresse von y = %p\t",&y); printf("Wert von y = %lf\n", y); Ausgabe: Adresse Adresse Adresse Adresse von von von von i j x y = = = = 0x28abf4 0x28abf0 0x28abe8 0x28abe0 Wert Wert Wert Wert von von von von i j x y = = = = 1 9 2.700000 3.141593 Speicheradressen Beispiel: Adressen von eindimensionalen Feldkomponenten char c[] ="Wetterwachs"; int a[] = {1,2,3}; printf("Adresse von c[0] printf("Adresse von c[1] printf("Adresse von c[2] printf("Adresse von c[3] printf("Adresse von a[0] printf("Adresse von a[1] printf("Adresse von a[2] = = = = = = = Ausgabe: Adresse Adresse Adresse Adresse von von von von c[0] c[1] c[2] c[3] = = = = 0x22ccb0 0x22ccb1 0x22ccb2 0x22ccb3 Adresse von a[0] = 0x22cca0 Adresse von a[1] = 0x22cca4 Adresse von a[2] = 0x22cca8 %p\n",&c[0]); %p\n",&c[1]); %p\n",&c[2]); %p\n",&c[3]); %p\n",&a[0]); %p\n",&a[1]); %p\n",&a[2]); Speicheradressen Beispiel: Adressen von mehrdimensionalen Feldkomponenten double A[3][2] = {{11,12},{21,22},{31,32}}; printf("Adresse von A[0][0] = %u\n",&A[0][0]); printf("Adresse von A[0][1] = %u\n",&A[0][1]); printf("Adresse von A[1][0] = %u\n",&A[1][0]); printf("Adresse von A[1][1] = %u\n",&A[1][1]); printf("Adresse von A[2][0] = %u\n",&A[2][0]); printf("Adresse von A[2][1] = %u\n",&A[2][1]); Adresse Adresse Adresse Adresse Adresse Adresse von von von von von von A[0][0] A[0][1] A[1][0] A[1][1] A[2][0] A[2][1] = = = = = = 2280560 2280568 2280576 2280584 2280592 2280600 Überlegungen: Wenn man die Adresse einer Variable kennt, wird man auch ihren Inhalt, d.h. ihren Wert manipulieren können. Da man direkt auf den Arbeitsspeicher zugreift, sollte dies auch von jeder Stelle im Programm aus möglich sein - vorausgesetzt die Adresse im Speicher ist bekannt. Zeiger: Definition und Prinzip Definition Ein Zeiger (engl. pointer ) ist eine Variable, deren Inhalt eine Speicheradresse und der zugehörige Datentyp ist. Funktionsprinzip: Zeiger ... Variable ... Arbeitsspeicher Zugriff über Variablennamen ... Variable ... Arbeitsspeicher Zugriff über Zeiger Variablenname steht stellvertretend für einen Speicherbereich Speicherbereich ist Inhalt der Zeigervariablen Verknüpfung ist mit Deklaration festgelegt und unveränderbar Dieser Inhalt ist selbstverständlich veränderbar → Zeiger kann „umgebogen“ werden Bei Zugriff wird die Speicher-Referenz intern aufgelöst und der Wert anschließend gelesen bzw. geschrieben. Auflösung der Speicherreferenz geschieht explizit, d. h. durch den Programmierer Zeiger Wozu überhaupt Zeiger? Indem man die Speicheradresse einer Variablen an eine Funktion übergibt, ermöglicht man es, den Inhalt der Variablen innerhalb der Funktion permanent zu verändern (Call by reference). Mit Zeigern lässt sich wie mit Feldern umgehen, mit dem Unterschied, dass sie als Parameter und als Rückgabewert von Funktionen verwendet werden können (dynamische Felder). Die gesamte Verwaltung von Speicherbereichen zur Laufzeit geschieht mit Hilfe von Zeigern (dynamische Speicherverwaltung). Zeiger ermöglichen es, aus einer Menge von Funktionen zur Ausführung einer bestimmten Aufgabe zur Laufzeit eine Variante auszuwählen (Callback-Prinzip). Zeiger Deklaration: Datentyp *Zeigername; bzw. Datentyp *z1,..., *zN; Achtung: vor jedem Zeigername muss ein „*“ stehen! Beispiel: int a, *p; erzeugt eine int-Variable und einen Zeiger auf int. Bedeutung des Datentyps: Wird über einen Zeiger auf eine Speicheradresse zugegriffen, so gibt der Datentyp an, wie viele Byte von der (Start-)Adresse an gelesen bzw. geschrieben werden sollen. Achtung: Fehlinterpretationen, d. h. die Verwendung eines Zeigers auf einen anderen Datentyp als vorgesehen, kann zu ungewollten Ergebnissen führen. Beispiel: Zeiger auf int statt Zeiger auf char. Eine char-Variable wurde deklariert int * char Fälschlicherweise wurde ein Zeiger auf int verwendet, um über die Speicheradresse auf die char-Variable zugreifen zu können. int Bei einem Zugriff werden statt 1 Byte 4 Byte angesprochen, von denen 3 nicht mehr zum „zulässigen“ Bereich gehören. Vor solchen Fällen warnt der Compiler bestenfalls! Arbeiten mit Zeigern: Referenzen Adress- oder Referenzoperator: & kann auf jedes beliebige Speicherobjekt (Variablen, Funktionen,. . . ) angewandt werden. hat Vorrang vor Vergleichs- und arithmetischen Operatoren, nicht jedoch vor dem „Array/Index-Operator“ []. liefert als Ergebnis einen konstanten Zeiger auf die Adresse (=Referenz) des Objekts. Beispiel: int i = 1; double x = M_E, y[] = {3.0, 0.0, 42.0}; printf("Adresse von i: %p\n", &i); printf("Wert von i: %d\n\n", i); printf("Adresse von x: %p\n", &x); printf("Wert von x: %lf\n\n", x); printf("Adresse von y[2]: %p\n", &y[2]); // oder &(y[2]) printf("Wert von y[2]: %lf", y[2]); Ausgabe: Adresse von i: 0x7fffb476d89c Wert von i: 1 Adresse von x: 0x7fff45786740 Wert von x: 2.718282 Adresse von y[2]: 0x7fff45786730 Wert von y[2]: 42.000000 Bemerkung: %p ist der Platzhalter für Adressen im Formatstring. Arbeiten mit Zeigern: Referenzen Inhalts- oder Dereferenzierungsoperator: * lässt sich auf Zeiger anwenden. steht auf der gleichen Prioritätsstufe wie der Adressoperator &. liefert den Wert, der an der Adresse gespeichert ist, auf die der Zeiger verweist. Beispiel: int i = 1; double x = M_E, *px = &x, y[] = {3.0, 0.0, 42.0}; printf("Adresse von i: %p\n", &i); printf("Wert von i: %d\n\n", *(&i)); printf("Adresse von x: %p\n", px); printf("Wert von x: %lf\n\n", *px); printf("Adresse von y: %p\n", y); printf("\"Wert\" von y: %lf\n", *y); Ausgabe: Adresse von i: 0x7fffb476d89c Wert von i: 1 Adresse von x: 0x7fff45786740 Wert von x: 2.718282 Adresse von y: 0x7fff45786720 “Wert” von y: 3.000000 Bemerkung: Der *-Operator kann auch auf Felder angewandt werden!? Arbeiten mit Zeigern: indirekter Zugriff auf Variablen Zeiger erlauben indirekten lesenden und schreibenden Zugriff auf zuvor deklarierte Variablen, indem man sie auf die entsprechenden Speicheradressen zeigen lässt. Beispiel: double x = M_PI, *p1 = &x, *p2; Ausgabe: Wert von x: 3.141593 printf("Wert von x: %lf\n\n", x); Zugriff via p1: x = 3.141593 printf("Zugriff via p1: x = %lf\n\n",*p1); Nach Manipulation, Zugriff via p1: x = -0.318310 x = -M_1_PI; printf("Nach Manipulation,\n"); printf("Zugriff via p1: x = %lf\n\n",*p1); p2=p1; *p2 = 0.1; printf("Nach Manipulation via p2,\n"); printf("direkter Zugriff: x = %lf\n",x); Nach Manipulation via p2, direkter Zugriff: x = 0.100000 Achtung: In der Deklaration steht „*p1“ nicht für Dereferenzierung, sondern dafür, dass es sich bei der folgenden Variablen um einen Zeiger handelt! Arbeiten mit Zeigern: „Umbiegen“ Einem Zeiger kann als Variable, deren Inhalt eine Adresse ist, (fast) jeder beliebige (Adress-)Wert zugewiesen werden. Man spricht dann vom „Umbiegen“ des Zeigers. Beispiel: int a = -5, b = 42, *p1 = &a, *p2 = &a; printf("p1 zeigt auf a:\n"); printf("&a = %p\n", &a); printf("p1 = %p\n\n", p1); p1 = &b; printf("Nach Umbiegen auf b:\n"); printf("&b = %p\n", &b); printf("p1 = %p\n\n", p1); p1 = p2; printf("Zurueckbiegen mit p2:\n"); printf("p1 = %p\n", p1); Ausgabe: p1 zeigt auf a: &a = 0x7fff72c3ecac p1 = 0x7fff72c3ecac Nach Umbiegen auf b: &b = 0x7fff72c3eca8 p1 = 0x7fff72c3eca8 Zurueckbiegen mit p2: p1 = 0x7fff72c3ecac Typische Pointer-Fehler I Falsche Annahme, dass Name und Funktion automatisch zusammenhängen. Beispiele: int a, *a; ; Fehler: In Konflikt stehende Typen für a double x = 3.14, *px; printf("%lf\n", *px); ; Warnung: px is used uninitialized in this function Vergessenes „&“ oder „*“ Beispiel: int a = 42, *pa; pa = a; printf("Wert von a: %d\n", pa); ; Bei der Zuweisung pa = a wird der Wert von a (42) als Adresse aufgefasst. ; In der printf-Anweisung wird die (vermeintliche) Referenz nicht aufgelöst, sondern die Adresse, die in pa gespeichert wurde, als int ausgegeben. Warnung: Durch (schreibenden) Zugriff auf uninitialisierte oder wild verbogene Zeiger oder Zeiger auf einen inkompatiblen Datentyp lässt sich jedes Programm ins Chaos stürzen (z. B. wenn durch den falschen Zugriff ein anderer Zeiger verbogen wird). Sonderfälle: void * und NULL Der Datentyp void * Ein Zeiger vom Typ void * ist kein „Zeiger auf Nichts“, sondern ein (universeller) Zeiger, der wie üblich eine Adresse speichert, dessen Typ jedoch (noch) nicht festgelegt ist. Einem Zeiger vom Typ void * können Adressen von typisierten Speicherobjekten zugewiesen werden. Durch einen korrekten Cast kann über den Zeiger auf Inhalte der Speicherobjekte zugegriffen werden. Dieser Cast sollte immer explizit sein. Der Nullzeiger NULL Formal ist NULL = (void *)0, d. h. die Zuweisungen p = NULL und p = 0 sind für einen Zeiger äquivalent. Verwendung von NULL erhöht aber die Lesbarkeit. Der Versuch, NULL mittels „*“ zu dereferenzieren, führt unmittelbar zu einem Speicherzugriffsfehler. Bei der Deklaration eines Zeigers ist es ratsam, mit NULL (bzw. einem anderen Wert) zu initialisieren. Dadurch vermeidet man ungewollte Zugriffe auf fremde Speicherbereiche. Viele Funktionen mit einem Zeiger-Datentyp als Rückgabewert liefern bei Scheitern NULL. Dieser Fall kann zur Kontrolle abgefragt werden. Typische Pointer-Fehler II Vergessener Cast Beispiel: int a; void *z = &a; printf("a = %d\n", *z); ; Warnung: Dereferenzierung eines void *-Zeigers ; Fehler: falsche Benutzung eines void-Ausdruckes Cast eines void * auf einen falschen Datentyp (Fehlinterpretation des void *) Beispiel: int a; void *z = &a; double *p = (double *)z; printf("a = %lf\n", *p); ; a = 573791543154187132539735239137854912273... (undefinierter Wert) Zuweisung statt Vergleich mit NULL Beispiel: int *p = NULL; p = ... if (p = NULL) return 1; ; Der Fall, dass p den Wert NULL hat, wird garantiert nicht wie beabsichtigt abgefangen. (Wieso nicht?) Call by reference Definition An eine Funktion werden Zeiger auf die Adressen von Variablen übergeben, mit deren Hilfe der Variableninhalt verändert werden kann. Zur Erinnerung: Call by value void vertausche(int a, int b) { int hilf = b; b = a; a = hilf; return; } Ergebnis: Beim Aufruf von vertausche in main passiert effektiv nichts, da nur die Werte der Funktionsparameter übergeben werden. Call by reference void vertausche(int *a, int *b) { int hilf = *b; *b = *a; *a = hilf; return; } In main: int x = 1, y = -2; vertausche(&x, &y); Ergebnis: Die Werte von x und y werden tatsächlich vertauscht! Wie funktioniert Call by reference? Beispiel: Eine Funktion fkt soll zwei Gleitkommazahlen als Ergebnis einer Berechnung liefern (z. B. die Koordinaten eines berechneten Punktes in R2 ). Mit Hilfe von return kann jedoch nur ein Wert zurückgegeben werden. Idee: Es werden zwei double-Variablen erg1 und erg2 an die Funktion übergeben, in welche die Ergebnisse gespeichert werden sollen. Der Programmcode sieht wie folgt aus: Deklaration von fkt als void fkt(double *e1, double *e2); Aufruf in main in der Form fkt(&erg1, &erg2); Definition von fkt enthält Zuweisungen *e1 = ...; *e2 = ...; Folgendes passiert bei der Ausführung: Durch Anwendung des &-Operators entstehen zwei Zeiger auf double. Diese beiden Zeiger werden als Parameter (in Übereinstimmung mit der Deklaration) an fkt übergeben. Die Funktion fkt „sieht“ zwar die beiden Variablen nicht, verfügt aber mit den Zeigern über ihre Adressen. Mit dem *-Operator wird die Referenz aufgelöst, und die Zuweisung von Werten an die betreffenden Speicherbereiche wird möglich. Fazit: Call by reference überwindet die Grenze des scope von Variablen. Typische Pointer-Fehler III Call by reference überwindet zwar die scope-Grenze, jedoch nicht die lifetime-Grenze! Beispiel: int *neuer_zeiger(void) { int a; return &a; } ; Nach Beendigung der Funktion ist die Lebenszeit der Variablen a vorbei. ; Der (gültige!) erzeugte Zeiger verweist auf irgendeine Speicherstelle, die längst anderweitig vergeben sein kann. Call by value mit Pointern statt Call by reference Beispiel: void biege(int *a, int *b) { int *hilf = b; b = a; a = hilf; return; } ; Die Speicherreferenzen werden nicht aufgelöst. Stattdessen finden (ausschließlich lokale!) Zuweisungen von Adressen an Zeigervariablen statt. ; Pointer, die als Parameter übergeben werden, bleiben unverändert. (Call by value!!) Zeigerarithmetik Erlaubte Operationen mit Zeigervariablen Addition und Subtraktion von Integer-Werten. Arithmetische Zuweisungen += und -= . Inkrement ++ und Dekrement -- . Differenzbildung zweier Zeiger. Beispiel: double x1=5.5, x2=7, *p1=&x1, *p2=&x2; printf("&x1 = %p \n" ,&x1); printf("&x2 = %p \n\n",&x2); printf("&p1 = %p, p1 = %p \n*p1 = %lf\n", &p1,p1,*p1); printf("&p2 = %p, p2 = %p \n*p2 = %lf\n\n", &p2,p2,*p2); p2 = p1 + 2; printf("&p2=%p, p2=%p \n*p2=%lf\n\n", &p2,p2,*p2); p2--;p2--; printf("&p2=%p, p2=%p \n*p2=%lf\n", &p2,p2,*p2); Ausgabe: &x1 = 0x28ac08 &x2 = 0x28ac00 &p1 *p1 &p2 *p2 = = = = 0x28abfc p1 = 0x28ac08 5.500000 0x28abf8 p2 = 0x28ac00 7.000000 &p2 = 0x28abf8 p2 = 0x28ac18 *p2 = 1807675861650890483 &p2 = 0x28abf8 p2 = 0x28ac08 *p2 = 5.500000 Zeiger und Felder Erinnerung: Der *-Operator lässt sich auf statische Felder anwenden und liefert das erste Feldelement. Frage: Wieso? Antwort: int main(void) { double x[] = {-3.14, 0.0, 4}; printf("Werte:\n"); printf(" *x = %lf\n", *x); printf("x[0] = %lf\n", x[0]); printf("\nAdressen:\n"); printf(" x = %p\n", x); printf(" &x = %p\n", &x); printf("&x[0] = %p\n", &x[0]); } return 0; Werte: *x = -3.140000 x[0] = -3.140000 Adressen: x = 0x7fffc291a0b0 &x = 0x7fffc291a0b0 &x[0] = 0x7fffc291a0b0 Fazit: Der Feldname ist zugleich ein konstanter Zeiger auf das erste Feldelement! ,→ Manipulationsversuche von x der Form x=x+2; x++; usw. scheitern. Folgerung: Mit einem Zeiger auf den Datentyp der Feldelemente kann man ebenfalls auf das Feld zugreifen! Zeiger und Felder Frage: Das erste Feldelement ist schon mal ein Anfang, aber wie erhält man die restlichen mit Hilfe eines anderen Zeigers? Antwort: Genau wie beim Original-Feld mit „[]“. int main(void) { double x[] = {-3.14, 0.1, 4}; double *p = x; // oder &x // oder &x[0] printf("x[2] = %lf\n", x[2]); printf("*(x+2) = %lf\n", *(x+2)); printf("\n*p++ = %lf\n",*p++); // dauerhafte Veraenderung von p! printf("*p = %lf\n", *p); printf(" p[0] = %lf\n", p[0]); printf(" p[1] = %lf\n", p[1]); } return 0; x[2] = 4.000000 *(x+2) = 4.000000 *p++ *p = p[0] p[1] = -3.140000 0.100000 = 0.100000 = 4.000000 Da die Elemente hintereinander im Speicher angeordnet sind, wird „x[n]“ intern interpretiert als „springe um n Speicherpositionen weiter und gib den Wert an dieser Stelle zurück“. In Pointer-Sprache ausgedrückt: *(x+n) Zeigerarithmetik: Zeiger und Felder Bemerkungen Klammer bei *(x+2) ist nötig, da der Inhaltsoperator * eine höhere Priorität als der Additionsoperator besitzt. Inkrement- und Inhaltsoperator sind beide von der selben Prioritätsstufe. Wichtig: der Inkrementoperator bezieht sich auf den Zeiger und nicht auf den dereferenzierten Inhalt. Der Zeiger wird dauerhaft umgesetzt, d.h. er zeigt nun auf eine andere Adresse im Speicher. Merke Sei A ein Feld von einem beliebigen Typ. Dann sind die jeweiligen Ausdrücke in den folgenden Boxen äquivalent: A[i] , *(A+i) Der zweite Ausdruck ist jedoch schwerer lesbar und wird als stilistisch schlecht angesehen. &A[i] A , , &A A+i , &A[0] , A+0 Zeigerarithmetik: Zeiger und Felder Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 24.05.2017 Zeiger und Felder Zur Erinnerung: Der Name eines statischen Feldes ist zugleich ein Zeiger auf das erste Feldelement. Auf Zeiger kann der Index/Array-Operator [] angewandt werden, mit dem gleichen Effekt wie bei statischen Feldern. Statische Felder können anstelle von Zeigern auf den gleichen Typ an Funktionen übergeben werden. Beispiel: Der folgende Code ist semantisch korrekt und liefert wie erwartet die Ausgabe „1. Element: 3.141500“. void zeigeErstesElement( double *array) { printf("1. Element: %lf\n", array[0]); return; } int main(void) { double x[2] = {3.1415, -1.0}; zeigeErstesElement(x); return 0; } Namen statischer Felder können als konstante Zeiger nicht umgebogen werden. Zuweisungen wie feld1 = feld2; schlagen fehl. Call by reference: Beispiel void vektor_ausgeben(double *vec,int n) // Funktion { int i; printf("("); for(i=0;i<n-1;i++) printf(" %2.1lf ,",vec[i]); //alternativ: *(vec+i) printf(" %2.1lf )\n",vec[n-1]); } void vektor_erhoehen(double *vec,int n,int summand) // Funktion { int i; for(i=0;i<n;i++) vec[i] +=summand; } int main(void) // Hauptprogramm { double vec[6]={1,1,1,1.5,1.5,1.5}; vektor_ausgeben(vec,6); vektor_erhoehen(vec,6,2); vektor_ausgeben(vec,6); } Ausgabe: ( 1.0 , 1.0 , 1.0 , 1.5 , 1.5 , 1.5 ) ( 3.0 , 3.0 , 3.0 , 3.5 , 3.5 , 3.5 ) Dynamische Speicherverwaltung Problem: Mit den bisherigen Mitteln können nur Speicherobjekte angelegt werden, deren Größe zur Compilezeit bekannt ist. Beispiel: Einfacher Texteditor Umsetzung: der aktuell bearbeitete Text wird zeilenweise in einem zweidimensionalen Feld (z. B. char text[zeilen][spalten]) gespeichert. Frage 1: Wie kann man im Vorfeld eine sinnvolle Zeilenbreite festlegen? Antwort: Gar nicht! Es sollte berücksichtigt werden, wie viele Zeichen zum Zeitpunkt der Ausführung auf dem Bildschirm in einer Zeile darstellbar sind. Frage 2: Wie viele Zeilen sollte das Feld umfassen, damit einerseits ein Arbeiten mit Texten „gängiger“ Länge möglich ist und andererseits das Programm nicht unnötig viel Speicher verbraucht? Antwort: Offenbar lassen sich nicht beide Bedingungen gleichzeitig erfüllen! Frage 3: Ist es sinnvoll, diese Größe festzulegen, ohne zu berücksichtigen, wie viel Speicher zur Verfügung steht, wie lang der bearbeitete Text tatsächlich ist und dass eventuell mehrere Texte gleichzeitig bearbeitet werden? Offensichtliche Antwort: Nein! Fazit: Mit diesen Speicherobjekten kommt man nicht besonders weit! Dynamische Speicherverwaltung Naheliegende Lösung: Das Programm soll zur Laufzeit entscheiden, wann wieviel Speicher benötigt wird. Programmiertechnische Umsetzung: mit Pointern! Im Code wird explizit ein Speicherblock einer bestimmten Größe angefordert, indem eine speziell dafür vorgesehene Funktion aufgerufen wird. Die Funktion versucht, den Speicher zu reservieren („allokieren“) und liefert bei Erfolg einen Zeiger auf die Startadresse des Speicherblocks, den sogenannten Basis-Zeiger (engl. base pointer ). Wird der Speicher nicht mehr benötigt, ruft man eine weitere Funktion auf, um Block explizit wieder freizugeben. Der Basis-Zeiger ist die einzige Referenz auf den allokierten Speicherblock (solange man keinen anderen Pointer ebenfalls dorthin zeigen lässt). Man darf ihn auf keinen Fall verlieren (umbiegen, Lebenszeit ablaufen lassen,. . . ), denn sonst kann der Block nicht mehr freigegeben werden. ; „speicherfressende“ Programme Funktionen zur Speicherverwaltung Header: #include <stdlib.h> Anforderung von Speicherplatz void *malloc(size_t size); (memory allocation) Fordert einen (uninitialisierten!) Speicherblock der Größe size Byte an und gibt einen Zeiger auf die Startadresse zurück. Bei Scheitern wir NULL zurückgegeben. void *calloc(size_t anzahl, size_t size); (cleared memory allocation) Fordert einen Speicherblock für anzahl Elemente der Größe size an und initialisiert mit Nullen. Ansonsten wie malloc. Vergrößerung / Verkleinerung eines Speicherblocks void *realloc(void *ptr, size_t size); Ändert die Größe des Blocks zum Basis-Zeiger ptr auf size Byte. Der Inhalt des alten Blocks bleibt unverändert, neu angeforderter Speicher bleibt uninitialisiert. Rückgabewert ist der zum neuen Speicherbereich gehörige Base-Pointer, welcher sich von ptr unterscheiden kann(!). Bei Scheitern wird nichts unternommen. Freigabe eines Speicherblocks void free(void *ptr); Gibt den Speicher frei, auf den ptr zeigt. Funktionen zur Speicherverwaltung Beispiel 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 7 int main(void) { int i; double *dyn_feld; 8 dyn_feld = (double *) malloc(5 * sizeof(double)); // Cast! // Platz fuer 5 double-Eintraege 9 10 11 for(i = 0; i < 5; i++) // Initialisierung dyn_feld[i] = 2.0 * i; for(i = 0; i < 5; i++) // Ausgabe printf("dyn_feld[%d] = %lf\n", i, dyn_feld[i]); 12 13 14 15 16 dyn_feld = (double *) realloc(dyn_feld, 8 * sizeof(double)); // Vergroesserung auf 8 double-Eintraege for(i = 5; i < 8; i++) dyn_feld[i] = 2.0 * i; // Initialisierung der neuen Eintraege for(i = 0; i < 8; i++) printf("dyn_feld[%d] = %lf\n", i, dyn_feld[i]); 17 18 19 20 21 22 23 free(dyn_feld); dyn_feld = NULL; 24 25 return 0; 26 27 } // Vorsichtsmassnahme Der sizeof-Operator Der Speicherbedarf wird niemals direkt als „magische“ Zahl angegeben, sondern immer mit Hilfe des sizeof-Operators berechnet. Erinnerung: sizeof(Datentyp) oder sizeof(Speicherobjekt) liefert den Speicherbedarf eines allgemeinen Datentyps oder eines deklarierten Speicherobjekts in Byte. Achtung: Bei statischen Feldern liefert sizeof die tatsächliche Größe des Feldes, bei dynamischen Feldern hingegen nur den Speicherbedarf der Pointervariable. Mit sizeof lässt sich die Größe eines dynamischen Feldes nicht feststellen! Bemerkung: Da Pointer Adressen speichern, ist sizeof(Datentyp *) unabhängig vom Datentyp immer gleich sizeof(size_t). Typische Fehler bei der Speicherverwaltung Nicht mehr benötigter Speicher wird vergessen freizugeben. double berechnung(double *vektor, int n) { double ergebnis, *zw_speicher = (double *) malloc(n*n * sizeof(double)); // Hier wird etwas berechnet return ergebnis; } Folge: Speicher bleibt bis zum Ende des Programms belegt, schlimmstenfalls wird der gesamte Speicher aufgebraucht und das System geht in die Knie. ; Speicher so früh wie möglich und in jedem Fall wieder freigeben Auf bereits freigegebenen Speicher wird zugegriffen. int *arr = (int *) malloc(2 * sizeof(int)); free(arr); arr[0] = -24; arr[1] = 42; printf("ptr2[0] = %d\n", arr[0]); printf("ptr2[1] = %d\n", arr[1]); Folge: Möglicherweise stehen dort noch immer die gleichen Werte, doch dafür gibt es keine Garantie. Vor einem solchen Zugriff wird nicht gewarnt. ; Basis-Pointer von freigegebenen Blöcken auf NULL setzen Typische Fehler bei der Speicherverwaltung Ein umgebogener Base-Pointer wird in free() verwendet. int i, *arr = (int *) malloc(20 * sizeof(int)); for (i = 0; i < 20; i++) *(arr++) = 5 * i; free(arr); Folge: Speicherzugriffsfehler Es werden statische statt dynamischer Felder verwendet. int feld[5], *ptr; ptr = feld; ptr[4] = 42; // Alternativer Zugriff // Code free(ptr); Folge: Speicherzugriffsfehler Dynamische Speicherverwaltung Speicherverwaltung während der Programmlaufzeit: Speicherbereich Verwendung Code Maschinencode des Programms Daten-Segment Statische und globale Variablen Stack (dt. Stapel) Funktionsaufrufe und lokale Variablen Heap (dt. Halde) Dynamisch reservierter Speicher Variablentypen Lokale, globale und statische Variablen Name und Typ werden im Programmcode durch eine Deklaration festgelegt. Variablen werden direkt über ihren Namen angesprochen. Scope und Lifetime sind durch die statische Struktur des Programms festgelegt. Speicherverwaltung erfolgt implizit: Lokale Variable werden im Stack angelegt. Globale und statische Variablen werden im Daten-Segment angelegt. Dynamische Variablen Erscheinen in keiner Deklaration, tragen keinen Namen. Variablen werden über ihre Adressen mit einem Pointer angesprochen. Scope und Lifetime folgen nicht den Regeln statischer Variablen. Speicherverwaltung im Heap geschieht explizit im Programm. Dynamische Implementierung von Vektoren Vorüberlegung: Vektoren x ∈ Rn lassen sich offenbar als Felder double x[n] implementieren. Um flexibler zu sein, bietet sich eine Implementierung als dynamisches Feld double *x in Verbindung mit den Speicherverwaltungsfunktionen an. Erzeugung eines neuen Vektors: double *neuerVektor(int laenge) { double *v; v = (double *) malloc(laenge * sizeof(double)); return v; // Bei Scheitern automatisch NULL } Erzeugung eines Nullvektors: double *neuerNullvektor(int laenge) { double *v; v = (double *) calloc(laenge, sizeof(double)); return v; // Bei Scheitern automatisch NULL } Dynamische Implementierung von Vektoren Ausgabe am Bildschirm: void zeigeVektor(double *vektor, int laenge) { int i; for (i = 0; i < laenge; i++) printf("%lf\n", vektor[i]); return; } Löschung: void loescheVektor(double *vektor) { free(vektor); return; } Dynamische Implementierung von Vektoren Kopie anlegen: void kopiereVektor(double *ziel, double *quelle, int laenge) { int i; for (i = 0; i < laenge; i++) ziel[i] = quelle[i]; return; } Weitere Funktionen: Arithmetische Operationen mit Vektoren (elementweise) Skalierung, Addition einer Konstanten (Betragsmäßig) maximales / minimales Element finden Untervektoren: Block, jedes n-te Element Permutationen Minimales Verwendungsbeispiel 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 7 8 double *neuerVektor(int laenge); void zeigeVektor(double *vektor, int laenge); void loescheVektor(double *vektor); void kopiereVektor(double *ziel, double *quelle, int laenge); 9 10 11 12 13 int main(void) { int i, N = 10; double *v = neuerVektor(N), *w = neuerVektor(N); 14 15 16 17 18 19 20 21 22 if (v==NULL || w==NULL) { printf("Zu wenig freier Speicher!"); free(v);free(w); return 1; } for (i = 0; i < N; i++) v[i] = 2.0 * i; 23 24 25 26 27 28 29 30 kopiereVektor(w, v, N); printf("w =\n"); zeigeVektor(w, N); loescheVektor(v); loescheVektor(w); return 0; } // Hier kommen noch die Funktionsdefinitionen Ausgabe: w = 0.000000 2.000000 4.000000 6.000000 8.000000 10.000000 12.000000 14.000000 16.000000 18.000000 Dynamische Implementierung von Matrizen Vorüberlegungen: Felder in C sind eindimensional, Matrizen jedoch (mindestens) zweidimensional. Jede Zeile einer 2D-Matrix kann als 1D-Vektor aufgefasst werden. Idee 1: Eine Matrix A ∈ Rm×n wird als Feld von Vektoren Ai ∈ Rn interpretiert. A1 A2 A2,3 A3 A4 Idee 2: Die Zeilen einer Matrix A ∈ Rm×n werden aneinandergereiht, so dass ein langer Vektor a ∈ Rm·n entsteht. Es gilt der Zusammenhang Ai,j = a(i−1)n+(j−1) . A1 A2 A3 a11 a A4 Dynamische Implementierung von Matrizen Umsetzung in C – Variante 1: Beispiel A ∈ R4×8 double ** double * A A[0] A[0][0] A[1] A[1][0] A[2] A[2][0] A[3] A[3][0] . . . . . . . . . . . . . . . . . . . . . . . . A[0][7] A[1][7] A[2][7] A[3][7] Ein Vektor v ist ein (dynamisches) Feld vom Typ double, implementiert als double *v. Folglich ist eine Matrix A ein (dynamisches) Feld von Vektoren A[i] vom Typ double *. Technisch gesehen ist A ein Zeiger auf double *, und jedes A[i] ist ein Zeiger auf double. Deklaration: double **A; Zugriff auf Feldelemente: Ai,j = ˆ A[i-1][j-1] Dynamische Implementierung von Matrizen: Funktionen Erzeugung einer Matrix: 1 2 3 4 double **neueMatrix(int n_zeilen, int n_spalten) { int i, j; double **matrix; 5 matrix = (double **) malloc(n_zeilen * sizeof(double *)); if (matrix == NULL) // Speicheranforderung gescheitert return NULL; 6 7 8 9 for (i = 0; i < n_zeilen; i++) { matrix[i] = (double *) malloc(n_spalten * sizeof(double)); if (matrix[i] == NULL) { for (j = 0; j < i; j++) free(matrix[j]); 10 11 12 13 14 15 16 17 free(matrix); return NULL; 18 19 } 20 } 21 22 return matrix; 23 24 } Dynamische Implementierung von Matrizen: Funktionen Bei der Speicheranforderung für die Matrix (double **) muss der Größenmultiplikator sizeof(double *) sein, denn jeder Eintrag in matrix ist schließlich ein double *! matrix = (double **) malloc(N_zeilen * sizeof(double *)); Scheitert die Anforderung, wird NULL zurückgegeben. if (matrix == NULL) return NULL; Ist die Speicheranforderung für eine Zeile A[i] erfolglos, so muss der bisher reservierte Speicher - also sämtliche Zeilenvektoren A[j] (j < i) sowie matrix selbst - wieder freigegeben werden, bevor die Funktion mit Rückgabewert NULL endet. if (matrix[i] == NULL) { for (j = 0; j < i; j++) free(matrix[j]); free(matrix); return NULL; } Dynamische Implementierung von Matrizen: Funktionen Ausgabe einer Matrix auf dem Bildschirm: 1 2 3 void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten) { int i, j; 4 for (i = 0; i < n_zeilen; i++) { for (j = 0; j < n_spalten; j++) printf("%.4lf ", matrix[i][j]); 5 6 7 8 9 printf("\n"); 10 } 11 12 return; 13 14 } Beachte: Die Ausgabe auf der Kommandozeile funktioniert nur zeilenweise. Nach dem Ende der Zeile den Newline-Character ’\n’ nicht vergessen! Dynamische Implementierung von Matrizen: Funktionen Löschung einer Matrix: 1 2 3 void loescheMatrix(double **matrix, int n_zeilen) { int i; 4 for (i = 0; i < n_zeilen; i++) free(matrix[i]); 5 6 7 free(matrix); 8 9 } Beachte: Es genügt nicht, das übergeordnete Feld matrix zu befreien. Jedes Feld matrix[i] muss einzeln an free() übergeben werden. Sicherheitshalber sollte nach dem Aufruf loescheMatrix(A, n); noch die Zuweisung A = NULL; vorgenommen werden, um zukünftige Zugriffe auf den freigegebenen Bereich zu verhindern. Anwendungsbeispiel 1 2 #include<stdio.h> #include<stdlib.h> 3 4 5 6 double **neueMatrix(int n_zeilen, int n_spalten); void zeigeMatrix(double** matrix, int n_zeilen, int n_spalten); void loescheMatrix(double** matrix, int n_zeilen); 7 8 9 10 11 int main(void) { int i, j, n = 10; double **A; 12 A = neueMatrix(n, n); 13 14 for (i = 0; i < n; i++) { for (j = 0; j < n; j++) A[i][j] = i+0.1*j; } 15 16 17 18 19 20 zeigeMatrix(A, n, n); loescheMatrix(A, n); 21 22 23 return 0; 24 25 } 26 27 // Ab hier noch die Funktionsdefinitionen Dynamische Implementierung von Matrizen Umsetzung in C – Variante 2: A1 A2 A3 A4 a11 a Deklaration: double *a; Als Funktionen zum Erzeugen und Löschen einer Matrix können die Vektor-Funktionen neuerVektor() und loescheVektor() verwendet bzw. umbenannt werden. Zum Zugriff auf das Element Ai,j verwendet man den Ausdruck a[i * n_spalten + j] . Umgekehrt lassen sich Zeilen- und Spaltenindex in a[index] berechnen mittels i = index / n_spalten; j = index % n_spalten; Vorteil gegenüber Doppelpointer-Variante: Vermeidung von Doppelschleifen → schneller bei Operationen auf der ganzen Matrix und bei Multiplikation. Viele numerische Programmbibliotheken (GSL, BLAS, NumPy,. . . ) verwenden intern diese Darstellung. Nachteil: Operationen basierend auf Zeilen- und Spaltenindex (z. B. transponieren) sind wegen des zusätzlichen Berechnungsschritts aufwändiger. Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 31.05.2017 Strukturen: Motivation Situation: Mit Funktionen verfügen wir über ein wirksames Mittel, um Programmcode sinnvoll zu gliedern und Komplexität zu verbergen. Für Daten haben wir bisher kein solches Konstrukt! Beispielproblem: Adressdatenbank Eine Adresse (in Deutschland) besteht aus Straße Hausnummer Stockwerk Postleitzahl Ort Zeichenkette positive Ganzzahl (Vorsicht: „21a“ nicht darstellbar!) positive Ganzzahl (optional) positive Ganzzahl Zeichenkette Umsetzung: jede Informationseinheit wird in einem entsprechenden Feld gespeichert, und die Einträge mit gleichen Feldindices gehören zueinander. char **strassen, unsigned *hausnummern, unsigned *stockwerke, unsigned *postleitzahlen, char **orte Nachteile: Unlogische Gruppierung von Daten, die miteinander nichts zu tun haben Zuordnung über den Feldindex ist fehleranfällig Strukturen Bessere Lösung: jede Adresse ist wie eine Variable ansprechbar, deren Elemente die einzelnen Informationseinheiten sind. Dazu gibt es in C das Konzept der Strukturen. Definition Eine Struktur ist die Zusammenfassung einer bestimmten Anzahl von Daten (möglicherweise) verschiedenen Typs zu einer Einheit, die mit einem festgelegten Namen angesprochen werden kann. Deklaration eines Strukturtyps: struct Etikett; Teilt dem Compiler mit, dass es einen Strukturtyp mit einem bestimmten Etikett (engl. structure tag) gibt. Muss außerhalb von main stehen. einer Strukturvariablen: struct Etikett Variablenname; Teilt dem Compiler mit, dass eine Variable eines bestimmten Namens gibt, die vom Typ struct Etikett ist. Definition eines Strukturtyps: struct Etikett {Codeblock}; Legt fest, welche Komponenten (engl. members) zu einer Struktur dieses Typs gehören. Muss außerhalb von main stehen (meistens direkt nach oder sogar statt der Deklaration). Strukturen Anwendung auf das Beispielproblem: 1 struct Adresse; // Deklaration des Strukturtyps "Adresse" struct Adresse { char *strasse; unsigned hausnr; int stock; unsigned plz; char *ort; }; // Definition der Struktur 2 3 4 5 6 7 8 9 10 // 0 = "keine Angabe" 11 12 13 14 15 16 17 18 19 int main(void) { // Deklaration einer Variablen vom Typ "struct Adresse" struct Adresse meine_adr; printf("sizeof(struct Adresse) = %lu\n", sizeof(struct Adresse)); printf("alternativ: sizeof(meine_adr) = %lu\n", sizeof(meine_adr)); return 0; } Die Definition macht in diesem Beispiel die Deklaration überflüssig. Anzahl und Namen der Komponenten der Struktur sind mit der Definition festgelegt und im Nachhinein nicht mehr veränderbar. Hinter der schließenden geschweiften Klammer muss ein Semikolon stehen! „struct Adresse“ kann wie ein gewöhnlicher Datentyp behandelt werden. Strukturen: Zugriff auf Komponenten Die Komponenten einer Struktur werden mit ihrem Namen angesprochen, im Gegensatz zum Zugriff über Indices bei Feldern. Das Pendant zum Index-Operator „[]“ ist der Strukturkomponenten-Operator „.“. Verwendung: Strukturvariable.Komponentenname Greift auf die Komponente des entsprechenden Namens einer zuvor deklarierten Strukturvariable zu. Der .-Operator hat wie [] höchste Priorität, insbesondere höher als *, &, ++, Cast und arithmetische Operatoren. Beispiel: meine_adr.strasse = "Stuhlsatzenhausweg"; meine_adr.hausnr = 103; meine_adr.stock = 0; meine_adr.plz = 66123; meine_adr.ort = "Saarbruecken"; printf("%s %u\n", meine_adr.strasse, meine_adr.hausnr); printf("%u %s\n", meine_adr.plz, meine_adr.ort); Ausgabe: Stuhlsatzenhausweg 103 66123 Saarbruecken Strukturen: Weitere Eigenschaften Initialisierung bei Deklaration: wie bei statischen Feldern in geschweiften Klammern struct Adresse meine_adr = {"Stuhlsatzenhausweg", 103, 0, 66123, "Saarbruecken"}; Strukturen können andere Strukturen enthalten (Schachtelung) struct Anschrift { char *nachname; char *vorname; struct Adresse adresse; }; Initialisierung: struct Anschrift meine_anschrift = {"mit Biergarten", "Restaurant", {"Stuhlsatzenhausweg", 103, 0, 66123, "Saarbruecken"} }; Zugriff auf geschachtelte Strukturen: z. B. meine_anschrift.adresse.hausnr Achtung: Zu tiefe Schachtelung macht Code unlesbar! Strukturen: Weitere Eigenschaften Zeiger auf Strukturen: struct Etikett *pointer Deklariert einen Zeiger auf den Datentyp struct Etikett Für den Zugriff (*pointer).Komponente auf Komponenten mit Hilfe des Pointers gibt es die vereinfachte Schreibweise pointer->Komponente Strukturen können einen Zeiger auf den eigenen Typ enthalten.Dazu müssen jedoch Deklaration und Definition getrennt werden: struct Anschrift; struct Anschrift { char *nachname; char *vorname; struct Adresse adresse; struct Anschrift *li_nachbar; struct Anschrift *re_nachbar; } meine_anschrift_; Zugriff auf Nachbarelement: z. B. Bemerkung: In diesem Programmcode wird in Verbindung mit der Definition der Struktur Anschrift eine globale Instanz meine_anschrift_ deklariert. meine_anschrift_.li_nachbar->hausnr Anwendung: Verkettung von Daten, z. B. Listen oder Bäume Strukturen: Weitere Eigenschaften Zeiger auf Strukturen können an Funktionen als Parameter übergeben und als Rückgabewert zurückgegeben werden. Strukturen, die andere Strukturen oder dynamische Speicherobjekte enthalten, sollten nicht mehr „von Hand“, sondern mit speziell dafür vorgesehenen Funktionen (Erstellung, Manipulation von Komponenten, Kopie, Löschung, . . . ) verwaltet werden ; Robuste Programme, Anfänge der „Objektorientierten Programmierung“ Vorsicht Strukturen können nur komponentenweise verglichen werden. Der Vergleich struct Vektor u,v; if (u==v) printf("Vektoren stimmen überein"); führt zu der Fehlermeldung: invalid operands to binary == Strukturen: Weitere Eigenschaften Vorsicht: Bei Zuweisung wird nur die oberste Ebene betrachtet. 1 2 3 4 5 6 struct Vektor { int dim; char *label; int *koord; }; 7 8 9 10 11 int main(void) { struct Vektor u = {2, "Ursprung", NULL}, e1; u.koord = (int *) malloc(u.dim * sizeof(int)); 12 e1 = u; // Shallow copy e1.label = "1. Einheitsvektor"; //umbiegen des Zeigers e1.label e1.koord[0] = 1; 13 14 15 16 17 printf("%s u = (%d,%d), ", u.label, u.koord[0], u.koord[1]); printf("%s e1 = (%d,%d)\n", e1.label, e1.koord[0], e1.koord[1]); 18 19 20 21 22 23 } free(u.koord); return 0; Ausgabe: Ursprung u = (1,0), 1. Einheitsvektor e1 = (1,0) ; Es wurden sämtliche Variablenwerte (= Adressen bei Zeigern!) kopiert, nicht jedoch das dynamische Feld! (Woher soll der Compiler auch davon wissen?) Typendefinition Mit Hilfe des Schlüsselworts typedef lassen sich in C eigene Datentypen definieren. Syntax: typedef AlterDatentyp NeuerDatentyp; Beispiel 1: primitive Datentypen typedef unsigned long int size_t; Anwendung: architekturunabhängige Programmierung Beispiel 2: in Verbindung mit Strukturen typedef struct Adresse Adresse; Im folgenden Code kann statt „struct Adresse“ einfach „Adresse“ geschrieben werden. Folge: erhöhte Lesbarkeit Die Typendefinition lässt sich mit der Strukturdefinition verbinden: typedef struct _Adresse_ Adresse; struct _Adresse_ { char *strasse; unsigned hausnr; int stock; unsigned plz; char *ort; }; oder äquivalent: typedef struct { char *strasse; unsigned hausnr; int stock; unsigned plz; char *ort; } Adresse; Beispiel: Erstellen/Kopieren eines Vektors 1 2 3 4 5 6 7 8 9 10 Vektor *neuerVektor(int dim, char *label, int *koord) { Vektor *vec=(Vektor *) malloc(sizeof(Vektor));int i; if(vec==NULL) printf("Zu wenig speicher"); vec->dim = dim; vec->koord = (int *) malloc(dim*sizeof(int)); for (i=0;i<dim;i++) vec->koord[i] = koord[i]; vec->label = dupliziereString(label); return vec;} 11 12 13 14 15 16 17 18 19 20 char *dupliziereString(char *string) {char *clone = NULL; if (string) { if ( !(clone = (char *) malloc(sizeof(char)*(strlen(string)+1)))) printf("Zu wenig Speicher!"); strcpy(clone,string); } return clone;} Ausschnitt aus dem Hauptprogramm: int dimension=2; koord=(int*) malloc(dimension*sizeof(int)); char ulabel[]="Ursprung"; struct Vektor *u=neuerVektor(dimension,ulabel,koord); //neuen Vektor erzeugen struct Vektor *e1=neuerVektor(u->dim,u->label,u->koord); //Vektor kopieren Beispiel: komplexe Zahlen Deklarationen und Definitionen 1 2 3 #include <stdio.h> #include <stdlib.h> #include <math.h> 4 5 6 7 8 9 typedef struct { double re; double im; } Complex; 10 11 12 13 Complex *newComplexPolar(double radius, double angle); Complex *complexProduct(Complex *z1, Complex *z2); //-------------------------------------------------------- 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Complex *newComplexPolar(double radius, double winkel) { Complex *z = (Complex *) malloc(sizeof(Complex)); z->re = radius * cos(winkel); z->im = radius * sin(winkel); return z; } //-------------------------------------------------------Complex *complexProduct(Complex *z1, Complex *z2) { Complex *z = (Complex *) malloc(sizeof(Complex)); z->re = z1->re*z2->re - z1->im*z2->im; z->im = z1->re*z2->im + z1->im*z2->re; return z; } Beispiel: komplexe Zahlen Hauptprogramm 1 2 3 4 5 int main(void) { Complex a = {1.0, 1.0}; Complex *b = newComplexPolar(2.0, 3*M_PI/4.0); Complex *c = complexProduct(&a, b); 6 printf("a = %lf+%lfi\n", a.re, a.im); printf("b = %lf+%lfi\n", b->re, b->im); printf("a*b = %lf+%lfi\n", c->re, c->im); c->re = 0; c->im = 2.5; b = complexProduct(&a, c); printf("a*c = %lf+%lfi\n", b->re, b->im); 7 8 9 10 11 12 13 14 15 16 } return 0; Ausgabe: a b a*b a*c = 1.000000+1.000000i = -1.414214+1.414214i = -2.828427+0.000000i = -2.500000+2.500000i Felder von Strukturen 1 2 3 4 5 int main(void) { int j, n = 10; Complex *dyn_vek = (Complex *) calloc(n, sizeof(Complex)); Complex stat_vek[10]; 6 for (j=0; j<n; j++) { dyn_vek[j].re = 2.0 * j; dyn_vek[j].im = 0.5 * j; stat_vek[j].re = 2.0 * j; stat_vek[j].im = 0.5 * j; } printf(" dyn_vek[7] = %lf+%lfi\n", dyn_vek[7].re, dyn_vek[7].im); printf("stat_vek[7] = %lf+%lfi\n", stat_vek[7].re, stat_vek[7].im); 7 8 9 10 11 12 13 14 15 16 17 18 19 } free(dyn_vek); return 0; Ausgabe: dyn_vek[7] = stat_vek[7] = 14.000000+3.500000i 14.000000+3.500000i ; Statische und dynamische Felder von Strukturen können wie gewohnt verwaltet werden. Allgemeine Merkregeln für Strukturen Strukturen dienen dazu, Einzelinformationen zu einer sinnvollen Einheit zusammenzufassen. Häufig sind Strukturen Abbilder von Konzepten aus dem Alltag oder von mathematischen Objekten (siehe Beispiele zu Adressen und komplexen Zahlen). Geschachtelte Strukturen sind möglich und oft sinnvoll, bergen aber die Gefahr, zu große Komplexität zu erzeugen und das Ziel größerer Ordnung zu verfehlen. Zudem steigt das Risiko von Programmierfehlern (s. shallow-copy -Problem). Strukturen, die andere Strukturen oder dynamische Speicherobjekte enthalten, sollten nicht mehr „von Hand“, sondern mit speziell dafür vorgesehenen Funktionen verwaltet werden (Erstellung, Manipulation von Komponenten, Kopie, Löschung, . . . ) ; Anfänge der „Objektorientierten Programmierung“ Zur Erhöhung der Lesbarkeit sollten Strukturen immer mit einem typedef als neuer Datentyp definiert werden. Zur Namensgebung hat sich folgende Konvention durchgesetzt: ErsterBuchstabeGrossOhneUnterstriche für eigene typedef-Datentypen _VorneUndHintenMitUnterstrichen_ für Strukturen, die nicht direkt, sondern nur in Verbindung mit typedef verwended werden. Beispiel: typedef struct _Adresse_ Adresse; Verwendung nur über Adresse, niemals über struct _Adresse_ Funktionszeiger: Vorüberlegungen Funktionsaufrufe sind bis jetzt im Code mit Name explizit angegeben („hartcodiert“) Folge: Zur Compilierzeit muss bekannt sein, welche Funktion eine bestimmte Aufgabe erfüllen soll. Beispielszenario: Wir haben einen Sortieralgorithmus geschrieben, der ein Feld von Zahlen sortiert. Dabei wollen wir flexibel bestimmen können, nach welchem Vergleichskriterium sortiert werden soll (größer, kleiner, 5. Ziffer in Dezimaldarstellung größer, Quersumme kleiner, . . . ). Mit den bisherigen Mitteln müsste im Algorithmus nach Vergleichskriterien unterschieden werden, z. B. mittels if-else oder switch. Alternativ müsste für jedes Vergleichskriterium eine separate Funktion geschrieben werden. Einleuchtendere Herangehensweise: Es gibt einen allgemeinen Sortieralgorithmus, der das Feld nach einer bestimmten Methode durchsucht und beim Vergleich zweier Elemente irgendein (variables!) Vergleichskriterium heranzieht. Zum Vergleich gibt es eine Reihe von Vergleichsfunktionen, die getrennt vom Sortierverfahren deklariert und definiert sind. Beim Aufruf des Sortieralgorithmus wird eine der Vergleichsfunktionen als Parameter mit angegeben. Genau dieses Verhalten lässt sich mit Funktionszeigern erzeugen! Funktionszeiger:Deklaration Datentyp (*FktZeigerName)(Parameter(typ)liste); Deklariert einen Zeiger auf eine Funktion, welche die Signatur Datentyp Funktion(Parameter(typ)liste) besitzt. Beispiele: void (*InitFkt)(double *, double); Zeiger auf eine Funktion, die einen double *- und einen double-Parameter nimmt und nichts (void) zurückgibt. double (*ZufGen)(void) Zeiger auf eine Funktion, die keine Parameter nimmt und einen double-Wert zurückgibt. double *(*NeuesFeld)(unsigned) Zeiger auf eine Funktion, die einen unsigned-Parameter nimmt und einen Pointer auf double zurückgibt. Funktionszeiger: Beispiel Deklaration und Definition von zwei Anzeigevarianten 1 #include <stdio.h> 2 3 4 void anzeigeVariante1(char *text); void anzeigeVariante2(char *text); 5 6 //-------------------------------------------------- 7 8 9 10 11 12 void anzeigeVariante1(char* text) { printf("\n %s\n", text); return; } 13 14 //-------------------------------------------------- 15 16 17 18 19 20 21 22 void anzeigeVariante2(char* text) { printf("\n **********************************"); printf("\n * %-30s *\n", text); printf(" **********************************\n"); return; } Funktionszeiger: Beispiel Hauptprogramm 1 2 3 int main(void) { void (*AnzFkt)(char *); 4 AnzFkt = anzeigeVariante1; AnzFkt("Test Variante 1"); 5 6 7 AnzFkt = anzeigeVariante2; AnzFkt("Test Variante 2"); 8 9 10 11 12 } return 0; Ausgabe: Test Variante 1 ********************************** * Test Variante 2 * ********************************** ; Vor dem Funktionsnamen in der Zuweisung steht kein &-Operator. Wie bei statischen Feldern hätte er keine Auswirkung. ; Beim Aufruf der Funktion via Pointer wird kein *-Operator benötigt. Die Schreibweise (*AnzFkt)(...) wäre äquivalent, nicht jedoch *AnzFkt(...)! Funktionszeiger: Bemerkungen Bei Zuweisungen mit Funktionspointern ist unbedingt darauf zu achten, dass die Signaturen von Pointer und Funktion übereinstimmen. Alles andere führt zu unkontrollierbarem Programmverhalten! Vor Fehlern dieser Art warnt der Compiler bestenfalls. Deklarationen von Zeigern auf Funktionen mit langer Parameterliste werden leicht unleserlich (vor allem bei Funktionen mit Funktionszeigern als Parameter): void funktion(int a, double b, double *(*f)(double, int, int, double *, double *)); int main(void) { double *(*fp)(double, int, int, double *, double *); fp = testfkt; // testfkt sei passend deklariert funktion(42, 3.14, fp); return 0; } Ein typedef schafft Abhilfe: typedef double *(*MeineFkt)(double, int, int, double *, double *); void funktion(int a, double b, MeineFkt f); Deklaration der Funktionspointer-Variable in main: MeineFkt fp; Rangfolge von Operatoren (Überblick) Priorität Priorität 1 Operator () [] . , -> Priorität 2 ! ++ , −− −,+ Assoziativität linksassoziativ Array/Index-Operator Member-Zugriff Logische Negation Unäres Plus, unäres Minus Adress-Operator * Dereferenzierung *,/ % rechtsassoziativ Inkrement, Dekrement & (type) Priorität 3 Bedeutung Funktionenaufruf Cast Multiplikation, Division linksassoziativ Modulo Priorität 4 +,− Priorität 6 <, <=, >, >= Priorität 7 == , ! = gleich, ungleich linksassoziativ Priorität 11 && logisches UND linksassoziativ Priorität 12 || logisches ODER linksassoziativ Plus, Minus linksassoziativ kleiner, ... linksassoziativ Merkregeln und lesen von Deklarationen Merke: Rechtsassoziativ sind lediglich: Zuweisungsoperatoren, Bedingungsoperator (? :) und unäre Operatoren. Sinnvolle Klammerungen können die Lesbarkeit von Code deutlich erhöhen! int * (*Funkzeiger)(); ↑ ↑ ↑ ↑ 4 2 3 1 Resultat: Funkzeiger ist ein Zeiger (1) auf eine Funktion (2) mit leerer Parameterliste, die einen Zeiger (3) auf Integer (4) zurückgibt. Publikumsfrage Was deklarieren die folgenden Statements? double *f(double *, int); ; Funktion, die als Parameter einen double * und einen int nimmt und einen double * zurückgibt. double (*f)(double *, int); ; Zeiger auf eine Funktion, die als Parameter einen double * und einen int nimmt und einen double zurückgibt. double *(*f)(double *, int); ; Zeiger auf eine Funktion, die als Parameter einen double * und einen int nimmt und einen double * zurückgibt. double *g[20]; ; g ist Feld mit 20 Einträgen vom Typ double *. Funktionszeiger: Beispiel Sortierverfahren In stdlib.h ist die folgende Sortierfunktion deklariert („quicksort“): void qsort(void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *)); Parameter: base Zu sortierendes Feld eines (noch) nicht festgelegten Datentyps nmemb Anzahl der Feldelemente size Größe eines Feldelements in Byte compar Zeiger auf eine (Vergleichs-)Funktion, die zwei void *-Zeiger auf zu vergleichende Elemente als Parameter nimmt und einen int zurückgibt. Interpretation: compar repräsentiert eine mathematische Ordnungsrelation „“ auf einer Menge M, d. h. für zwei beliebige Werte a, b ∈ M gilt entweder a b, b a oder beides. Die zu vergleichenden Elemente von base stammen aus M. Der Rückgabewert von compar ist -1 (a b), 0 (Gleichheit) oder +1 (b a), wobei a dem ersten und b dem zweiten Parameter von compar entspricht. Funktionszeiger: Beispiel Sortierverfahren Anwendung: 1 2 #include <stdio.h> #include <stdlib.h> 3 4 int unsign_qsumme_kleiner(const void *pa, const void *pb); 5 6 7 8 9 int main(void) { unsigned z[5] = {23, 511, 10100, 8, 333}; qsort(z, 5, sizeof(unsigned), unsign_qsumme_kleiner); 10 11 12 13 14 } printf("Sortiertes Feld:\n"); printf("%d %d %d %d %d\n", z[0], z[1], z[2], z[3], z[4]); return 0; 15 16 17 18 19 int unsign_qsumme_kleiner(const void *pa, const void *pb) { unsigned a = *((unsigned *) pa), b = *((unsigned *) pb); unsigned qs_a = a % 10, qs_b = b % 10; 20 while (a /= 10) qs_a += a % 10; 21 22 23 while (b /= 10) qs_b += b % 10; 24 25 26 27 28 } return (qs_a < qs_b)? -1 : (qs_b < qs_a)? +1 : 0; Funktionszeiger: Beispiel Sortierverfahren – Codeanalyse Deklaration und Definition der Sortierfunktion unsign_qsumme_kleiner soll die Quersumme von unsigned-Werten vergleichen. Die Signatur muss int fkt(const void *, const void *) sein. Im Funktionsrumpf werden die Parameter pa und pb zunächst als unsigned * gecastet, anschließend dereferenziert und die Werte in unsigned-Variablen a und b geschrieben. Exemplarisch für pa: unsigned a = *( (unsigned *) pa ); In den Schleifen werden die Quersummen von a und b berechnet und in qs_a bzw. qs_b gespeichert. Beachte: while (a /= 10) führt zuerst Ganzzahl-Division durch und prüft anschließend, ob das Ergebnis ungleich 0 ist (in diesem Fall besteht a noch nicht aus einer einzigen Dezimalziffer). Die return-Zeile verwendet die verkürzte Fallunterscheidung (Bedingung)? Wert_Falls_Ja : Wert_Falls_Nein; Beispiel: absx = (x > 0)? x : -x; speichert genau wie if (x > 0) absx = x; else absx = -x; den Betrag von x in absx. Funktionszeiger: Beispiel Sortierverfahren – Codeanalyse Hauptprogramm Das Feld z mit 5 unsigned-Einträgen soll aufsteigend bzgl. der Quersumme sortiert werden. Dazu wird qsort mit den Parametern z (base), 5 (nmemb), sizeof(unsigned) (size) und unsign_qsumme_kleiner (compar) aufgerufen: qsort(z, 5, sizeof(unsigned), unsign_qsumme_kleiner); Ausgabe: Sortiertes Feld: 10100 23 511 8 333 Fazit: Wie qsort genau funktioniert, ist hier völlig unerheblich. Entscheidend ist, dass die Funktion ein Feld anhand einer gegebenen Vergleichsroutine sortiert. Kommandozeilenargumente Bisher: $ gcc -o ProgName C_Code.c $ ./ProgName . . . Neu: Komandozeilenargumente Die main-Funktion lässt sich auch mit zwei Parametern aufrufen. Vollständige Deklaration von main: int main(int argc, char *argv[]) argc argument counter: Anzahl der beim Programmaufruf übergebenen Argumente, einschließlich des Programmnamens. Erst wenn argc > 1 ist, werden tatsächlich Parameter übergeben. argv argument vector: Feld von Strings Bemerkung: Die Namen argc und argv (auch üblich: args) sind Konvention und nicht zwingend festgelegt. Möglich (aber wenig sinnvoll) wäre auch int main(bla, blubb) Kommandozeilenargumente Beispiel: argumente.c 1 #include <stdio.h> 2 3 4 5 int main(int argc, char *argv[]) { int i; 6 printf("Anzahl der Argumente: %d\n\n", argc); 7 8 for (i=0; i<=argc; i++) printf("Argument %d: %s\n", i, argv[i]); 9 10 11 12 13 } return 0; Ausgabe: Der Aufruf ./argumente arg1 arg2 ––help +~ 125.777 -99 liefert Anzahl der Argumente: 7 Argument Argument Argument Argument 0: 1: 2: 3: ./argumente arg1 arg2 --help Argument Argument Argument Argument 4: 5: 6: 7: +~ 125.777 -99 (null) Kommandozeilenargumente Beachte: Parameter werden durch Leerzeichen getrennt. Ausdrücke wie -o Ausgabe werden als zwei separate Argumente aufgefasst. Jedes Argument wird als Zeichenkette in argv gespeichert. Die Reihenfolge der Strings in argv entspricht dabei der Reihenfolge auf der Kommandozeile. Werden Zahlen als Argumente übergeben, müssen diese mit Hilfe der entsprechenden Umwandlungsfunktionen (z. B. atoi oder atof) in ein Zahlenformat konvertiert werden. Mit String-Vergleichsfunktionen (z. B. strncmp) lässt sich beispielsweise prüfen, ob ein Programm mit einem bestimmten Optionsparameter aufgerufen wurde. Da jedoch die Reihenfolge der Optionsargumente festgelegt ist, benötigen Programme mit vielen Optionen einen flexibleren Ansatz zur Auswertung der Kommandozeile (→ Parser). Kommandozeilenargumente: Beispiel 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int main(int argc, char *argv[]){ double x, y, z; if (argc < 4){ // Es fehlen Argumente printf("\nKorrekter Aufruf: "); printf("%s zahl1 op zahl2\n", argv[0]); return 1; } x = atof(argv[1]); y = atof(argv[3]); switch (argv[2][0]) { case '+': z = x + y; break; case '-': z = x - y; break; case 'x': case '*': z = x * y; break; case '/': z = x / y; break; default: printf("\nFalscher Operator! ABBRUCH!\n"); return -1; } 27 28 29 30 } printf("\n%s %s %s = %lf", argv[1], argv[2], argv[3], z); return 0; Kommandozeilenargumente $ gcc -o berechne taschenrechner.c $ ./berechne 2 + 5 2 + 5 = 7.000000 $ ./berechne 2 x 5 2 x 5 = 10.000000 $ ./berechne 2 / 5 2 / 5 = 0.400000 $ ./berechne 2 / Korrekter Aufruf: ./berechne zahl1 op zahl2 Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 07.06.2017 Fortgeschrittene Ein- und Ausgabe Bisher: Ein- und Ausgabe nur über die Kommandozeile Erweiterung: Konzept des Datenstroms (engl. data stream) Bezeichnet allgemein eine Folge von Datensätzen gleichen Typs, deren Ende nicht im Voraus bekannt ist. Lässt sich nicht als Ganzes, sondern nur sequentiell verarbeiten. Kann „vor- und zurückgespult“ werden. Ein spezieller „End-of-file“-Indikator dient zur Markierung des Endes. In C wird auf Datenströme mit Variablen vom Typ FILE * zugegriffen. FILE ist eine Struktur und enthält u.a. einen Zeiger auf die aktuell zu verarbeitende Position im Datenstrom. Standard-Datenströme #include <stdio.h> stdin (standard input): Einlesen von Daten über die Tastatur stdout (standard output): (gepufferte) Ausgabe von Daten auf der Kommandozeile stderr (standard error): Ausgabe auf der Kommandozeile zur Fehlerbehandlung Dateiströme: Zeiger vom Typ FILE * können mit Hilfe spezieller Funktionen mit Dateien verbunden werden. Fortgeschrittene Ein- und Ausgabe: Funktionen Öffnen einer Datei FILE *fopen(const char *path, const char *mode); Erzeugt einen Datenstrom aus der Datei, die unter dem Pfad path liegt. Rückgabewert ist ein FILE *, der entweder an den Anfang oder das Ende des neuen Dateistroms zeigt. Bei Scheitern - z. B. Öffnen einer nicht vorhandenen Datei oder fehlende Dateirechte - liefert die Funktion NULL zurück. Der String path gibt den Pfad zur Datei im Dateisystem an, z. B. “../Daten/werte.dat” (relativer Pfad) oder “/usr/include/stdio.h” (absoluter Pfad). Der Modus mode gibt an, in welche Richtung der Strom fließen kann. Mögliche Modi sind “r”, “w”, “a”, “r+”, “w+” und “a+”: Modus “r” “w” “a” “r+” “w+” “a+” Bedeutung read only write only append only read/write read/write append/write Zeigerposition Dateianfang Dateianfang Dateiende Dateianfang Dateianfang Dateiende Dateiinhalt unverändert wird gelöscht unverändert unverändert∗ wird gelöscht unverändert ∗ Bezieht sich nur auf den Zeitpunkt des Öffnens Beispiel: FILE *strom = fopen(“Dokument.txt”, “r”); Datei existiert Rückgabewert Neue Datei Rückgabewert Rückgabewert Neue Datei Neue Datei nicht NULL NULL NULL Fortgeschrittene Ein- und Ausgabe: Funktionen Schließen eines Datenstroms int *fclose(FILE *stream); Schließt den Datenstrom stream und gibt 0 zurück bei Erfolg, andernfalls die Konstante EOF (End Of File). Wichtig: Zu jedem fopen() gehört ein fclose()! Position herausfinden long ftell(FILE *stream); Gibt die aktuelle Position des Zeigers in stream als Adressabstand vom Anfang des Stromes (offset) in Byte an. Vor- und Zurückspulen int fseek(FILE *stream, long offset, int whence); Versetzt die Position des Zeigers in stream um offset Byte. Je nach Wert von whence wird der Versatz relativ zum Anfang (whence = SEEK_SET), zum Ende (whence = SEEK_END) oder zur aktuellen Position (whence = SEEK_CUR) gerechnet. Der Rückgabewert ist 0 bei Erfolg, andernfalls ungleich 0. void rewind(FILE *stream); Spult den Strom stream an den Anfang zurück. Der Aufruf rewind(stream); ist äquivalent zu fseek(stream, 0, SEEK_SET); (bis auf interne Feinheiten). Fortgeschrittene Ein- und Ausgabe: Funktionen Aus einem Datenstrom lesen (Text) int fscanf(FILE *stream, const char *format, ...); Genau wie scanf, wobei statt aus stdin aus dem Datenstrom stream gelesen wird. (Deshalb ist scanf(...) äquivalent zu fscanf(stdin, ...).) Rückgabewert ist die Anzahl der gelesenen Zeichen. int fgetc(FILE *stream); Liest ein Zeichen aus stream, das als int gecastet zurückgegeben wird. Im Fehlerfall oder bei Dateiende ist der Rückgabewert EOF. char *fgets(char *s, int n, FILE *stream); Liest aus stream und schreibt das Ergebnis in den Puffer, auf den s zeigt. Das Lesen wird abgebrochen, wenn entweder das Zeilenende \n oder das Dateiende erreicht ist oder n − 1 Zeichen gelesen wurden. Im Fehlerfall oder bei Dateiende ist der Rückgabewert NULL. Beachte: Nach dem Aufruf einer dieser Funktionen zeigt stream auf die erste Position im Anschluss an den bearbeiteten Bereich. Fortgeschrittene Ein- und Ausgabe: Funktionen In einen Datenstrom schreiben (Text) int fprintf(FILE *stream, const char *format, ...); Arbeitet wie printf, wobei in den Datenstrom stream geschrieben wird. (Deshalb ist printf(...) äquivalent zu fprintf(stdout, ...).) Rückgabewert ist die Anzahl der geschriebenen Zeichen. int fputc(int c, FILE *stream); Schreibt das als unsigned char gecastete Zeichen c in den Strom stream. Das Zeichen wird als int zurückgegeben, im Fehlerfall EOF. int *fputs(const char *s, FILE *stream); Schreibt den String, auf den s zeigt, ohne das Nullzeichen \0 in den Strom stream. Im Fehlerfall wird EOF zurückgegeben. Beachte: Nach dem Aufruf einer dieser Funktionen zeigt stream auf die erste Position im Anschluss an den bearbeiteten Bereich. Fortgeschrittene Ein- und Ausgabe: Beispiel 1 #include <stdio.h> 2 3 4 5 int main(void) { FILE *datenstrom; 6 datenstrom = fopen("Testdatei.txt", "r"); if (datenstrom == NULL) { fprintf(stderr, "Fehler: Testdatei.txt konnte nicht geoeffnet werden!\n"); return -1; } 7 8 9 10 11 12 13 fseek(datenstrom, 0, SEEK_END); // Ans Ende spulen fprintf(stdout, "Offset am Ende: %lu\n", ftell(datenstrom)); 14 15 16 fseek(datenstrom, -5, SEEK_CUR); // 5 Bytes zurueck fprintf(stdout, " nach Zurueckspulen: %lu\n", ftell(datenstrom)); 17 18 19 rewind(datenstrom); fprintf(stdout, " 20 21 22 fclose(datenstrom); 23 24 25 26 } return 0; // Zurueckspulen am Anfang: %lu\n", ftell(datenstrom)); Fortgeschrittene Ein- und Ausgabe: Beispiel Datei Testdatei.txt: Hallo Welt! Die Datei enthält genau eine Zeile „Hallo Welt!“ (11 Characters = 11 Byte). Ausgabe des Programms: Offset am Ende: 11 nach Zurueckspulen: 6 am Anfang: 0 Beispiel: Matrix aus Datei einlesen (Funktion) 1 2 3 4 double **neueMatrixAusDatei(FILE *strom, int *n_zeilen, int *n_spalten) { int i, j; double **matrix; 5 while (fgetc(strom) == '#') // Kommentarzeichen->Zeile ueberspringen while (fgetc(strom) != '\n'); fseek(strom, -1, SEEK_CUR); // Eine Position zurueck 6 7 8 9 fscanf(strom, " n = %d, m = %d\n", n_zeilen, n_spalten); matrix = neueMatrix(*n_zeilen, *n_spalten); 10 11 12 13 14 15 16 17 18 19 } for (i=0; i<*n_zeilen; i++) { for (j=0; j<*n_spalten; j++) fscanf(strom, "%lf", &matrix[i][j]); } return matrix; Jeder Aufruf von fgetc erhöht die Position von strom um 1. Deshalb zeigt strom nach der (leeren) Schleife in Zeile 7 auf den Anfang der nächsten Zeile. Insgesamt überspringen die Zeilen 6 und 7 Dateizeilen, die mit ’#’ beginnen. Zeile 10 erwartet einen Text wie n = 6, m = 1. Die gelesenen Zahlen werden in n_zeilen und n_spalten (beides Zeiger auf int-Variablen) gespeichert. Für fscanf gilt: Leerzeichen und Zeilenumbrüche innerhalb der Platzhalter werden automatisch übersprungen. Ein Leerzeichen im Formatstring steht für eine beliebige Anzahl (auch 0) von tatsächlich zu lesenden Leerzeichen. Beispiel: Matrix aus Datei einlesen (Hauptprogramm) 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten); void loescheMatrix(double **matrix, int n_zeilen); double **neueMatrixAusDatei(FILE *strom, int *n_zeilen, int *n_spalten); 7 8 9 10 11 12 13 int main(void) { int n, m; FILE *fp; if ( (fp = fopen("Matrix.dat", "r")) == NULL ) return 1; double **H = neueMatrixAusDatei(fp, &n, &m); 14 15 16 17 18 19 } zeigeMatrix(H, n, m); loescheMatrix(H, n); fclose(fp); return 0; 20 21 // Funktionsdefinitionen ... Ausgabe: 1.0000 0.5000 0.3333 0.2500 0.5000 0.3333 0.2500 0.2000 0.3333 0.2500 0.2000 0.1667 0.2500 0.2000 0.1667 0.1429 0.2000 0.1667 0.1429 0.1250 0.1667 0.1429 0.1250 0.1111 0.1429 0.1250 0.1111 0.1000 Beispiel: Matrix aus Datei einlesen (Matrix.dat) Version 1 n=4, m=7 1.0 0.5 0.333333 0.25 0.500000 0.333333 0.25 0.2 0.333333 0.25 0.2 0.166667 0.250000 0.2 0.166667 0.142857 0.2 0.166667 0.142857 0.125 0.166667 0.142857 0.125 0.111111 0.142857 0.125 0.111111 0.1 Version 2 # Das ist die Hilbertmatrix H_ij = 1/(i+j-1) # # Hoffentlich werden diese Zeilen uebersprungen... n = 4, m = 7 1.0 0.5 0.333333 0.25 0.500000 0.333333 0.25 0.2 0.333333 0.25 0.2 0.166667 0.250000 0.2 0.166667 0.142857 0.2 0.166667 0.142857 0.125 ; Beide Versionen werden korrekt eingelesen! 0.166667 0.142857 0.125 0.111111 0.142857 0.125 0.111111 0.1 Beispiel: Matrix in Datei schreiben (Funktion) 1 2 3 4 5 6 void schreibeMatrixInDatei(FILE *strom, double **matrix, int n_zeilen, int n_spalten) { int i, j; fprintf(strom, "# Automatisch generiert mit schreibeMatrixInDatei\n\n"); fprintf(strom, "n = %d, m = %d\n\n", n_zeilen, n_spalten); 7 for (i=0; i<n_zeilen; i++) { for (j=0; j<n_spalten; j++) fprintf(strom, "%.6lf ", matrix[i][j]); 8 9 10 11 12 13 } 14 fprintf(strom, "\n"); 15 16 17 } return; Beispiel: Matrix in Datei schreiben (Hauptprogramm) 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 7 void loescheMatrix(double **matrix, int n_zeilen); double **neueMatrixAusDatei(FILE *strom, int *n_zeilen, int *n_spalten); void schreibeMatrixInDatei(FILE *strom, double **matrix, int n_zeilen, int n_spalten); 8 9 10 11 12 13 int main(void) { int n, m; FILE *fp; double **H; 14 fp = fopen("Matrix.dat", "r"); H = neueMatrixAusDatei(fp, &n, &m); fclose(fp); 15 16 17 18 fp = fopen("Matrix_generiert.dat", "w"); schreibeMatrixInDatei(fp, H, n, m); fclose(fp); 19 20 21 22 loescheMatrix(H, n); 23 24 25 26 } return 0; Beispiel: Matrix in Datei schreiben Inhalt von Matrix_generiert.dat: # Automatisch generiert mit schreibeMatrixInDatei n = 4, m = 7 1.000000 0.500000 0.333333 0.250000 0.500000 0.333333 0.250000 0.200000 0.333333 0.250000 0.200000 0.166667 0.250000 0.200000 0.166667 0.142857 0.200000 0.166667 0.142857 0.125000 0.166667 0.142857 0.125000 0.111111 0.142857 0.125000 0.111111 0.100000 Speicherung als Text oder binär: Vergleich Dateigröße Binär: Produkt aus Anzahl der Elemente und Größe eines Elements in Byte. Text: Variiert mit der textuellen Länge der gespeicherten Elemente. Meistens größer als im Binärformat. Präzision Binär: Entspricht der Genauigkeit des Datentyps. Text: Der Genauigkeit sind prinzipiell keine Grenzen gesetzt, jedoch kann der Speicherbedarf dadurch sehr groß werden. Beispiel: Um die größtmögliche float-Zahl (3.4 · 1038 ) als Text zu speichern, benötigt man 38 Zeichen = ˆ 38 Byte, im Binärformat nur sizeof(float) = 4 Byte! Metadaten Binär: Üblicherweise ist die Datei unterteilt in einen Header und die Daten. Es muss genau beschrieben werden, welche Bytes wofür stehen. Beispiel Matrix: Bytes 1-4 – unsigned – Länge des Headers in Byte, 5-8 – unsigned – Anzahl der Zeilen, 9-12 – unsigned – Anzahl der Spalten, 13-Ende – double – Matrixelemente. Text: Informationen über die Daten können in die Datei geschrieben werden. Vor- und Nachteile der binären Speicherung Vorteile: Allein der Datentyp bestimmt die Größe des benötigten Speicherplatzes. Es wird meistens deutlich weniger Speicherplatz als für die Textvariante benötigt. Maschinennahe Ein- und Ausgabe, daher deutlich schneller. Nachteile: Binäre Daten sind nicht direkt vom Menschen lesbar. Binärformate sind vom System abhängig, d.h. Daten müssen unter Umständen zuerst umgewandelt werden. Fortgeschrittene Ein- und Ausgabe: Funktionen Aus einem Datenstrom lesen (binär) size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); Liest nmemb Elemente der Größe size aus dem Datenstrom stream und speichert sie (in der gleichen Reihenfolge) im Feld, auf das ptr zeigt. Rückgabewert ist die Anzahl der erfolgreich gelesenen Elemente. Beachte: Die Daten werden nicht als Text, sondern als Bitfolgen interpretiert. In einen Datenstrom schreiben (binär) size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); Schreibt nmemb Elemente der Größe size aus dem Feld, auf das ptr zeigt, in den Datenstrom stream. Rückgabewert ist die Anzahl der erfolgreich geschriebenen Elemente. Beispiel: binäres Lesen und Schreiben (Funktionen) 1 2 3 4 void schreibeMatrixInBinDatei(char *pfad, double **matrix, int n_zeilen, int n_spalten) { int i; FILE *fp; 5 if ( (fp = fopen(pfad, "wb")) == NULL ) return; 6 7 for (i=0; i<n_zeilen; i++) fwrite(matrix[i], sizeof(double), n_spalten, fp); 8 9 10 11 12 13 } fclose(fp); return; 14 15 16 17 18 double **neueMatrixAusBinDatei(char *pfad, int n_zeilen, int n_spalten) { int i; double **matrix; FILE *fp; 19 if ( (fp = fopen(pfad, "rb")) == NULL ) return NULL; 20 21 matrix = neueMatrix(n_zeilen, n_spalten); for (i=0; i<n_zeilen; i++) fread(matrix[i], sizeof(double), n_spalten, fp); 22 23 24 25 26 27 28 } fclose(fp); return matrix; Beispiel: binäres Lesen und Schreiben (Hauptprogramm) 1 2 3 double **neueMatrixAusBinDatei(char *pfad, int n_zeilen, int n_spalten); void schreibeMatrixInBinDatei(char *pfad, double **matrix, int n_zeilen, int n_spalten); 4 5 6 7 8 int main(void) { int n = 4, m = 7; double **H; 9 H = neueMatrixAusBinDatei("Matrix.bin", n, m); zeigeMatrix(H, n, m); schreibeMatrixInBinDatei("Matrix_kopie.bin", H, n, m); loescheMatrix(H, n); 10 11 12 13 14 15 16 } return 0; Beachte: Die Funktionen werden (zur Abwechslung) nicht mit dem bereits geöffneten Strom, sondern mit dem Pfad zum Dateinamen aufgerufen. In den if-Statements wird zunächst der Datenstrom geöffnet und anschließend auf NULL überprüft. Da jede Matrixzeile als separates Feld gespeichert ist (zweistufige Speicherung mit double **), muss auch zeilenweise in die Datei geschrieben werden. C-Präprozessor Wir haben bereits die Direktive #include <Headerdatei> verwendet, um „fremde“ Funktionsdeklarationen zu importieren Generell ist der Präprozessor dafür zuständig, Text durch anderen Text zu ersetzen. Der Präprozessor wird vor dem Compiler aufgerufen, deshalb muss der verarbeitete Code syntaktisch korrekt sein. Präprozessordirektiven beginnen stets mit einer Raute # und stehen im Code bis auf wenige Ausnahmefälle ganz zu Beginn. Die wichtigsten Direktiven sind Einfügungen, Makros und bedingte Ersetzungen. C-Präprozessor: Einfügungen Standard-Header Syntax: #include <systemheader.h> Fügt den Text der Headerdatei systemheader.h an der Stelle im Code ein, an welcher der #include-Befehl steht. Die Datei wird im Standard-Header-Pfad des Systems gesucht. Bei Unix-Systemen ist dies üblicherweise /usr/include. Beispiel: #include <stdio.h> bindet die Datei /usr/include/stdio.h ein. Bei Headern in Unterordnern muss dieser mit angegeben werden, z. B. #include <sys/time.h> für den Header /usr/include/sys/time.h. Lokale (eigene) Header Syntax: #include “lokaler_header.h” Bindet die Datei lokaler_header.h ein, die im aktuellen Verzeichnis liegt. Will man einen Header aus einem anderen Pfad inkludieren, muss dem Compiler der Pfad mitgeteilt werden. Beispiel: Zum Einbinden des Headers matrix.h im Unterordner meine_header wird im Quellcode die Zeile #include “matrix.h” eingefügt. der Compiler mit der Option -Imeine_header aufgerufen. Die Option -IOrdner macht die Header in Ordner für den Compiler sichtbar. C-Präprozessor: Einfügungen – Beispiel matrix.h Headerdatei matrix.h im Unterordner meine_header (Ausschnitt): 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 // Legt Speicher fuer eine neue Matrix an und liefert einen Zeiger auf die // erste Zeile; bei Misserfolg NULL double **neueMatrix(int n_zeilen, int n_spalten); 7 8 9 // Gibt die Matrix am Bildschirm aus void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten); Programmcode matrix_test.c: 1 #include "matrix.h" 2 3 4 5 6 7 8 int main(void) { int n = 4, m = 7; double **H = neueMatrixAusBinDatei("Matrix.bin", n, m); zeigeMatrix(H, n, m); loescheMatrix(H, n); 9 10 11 return 0; } // Hiernach die Funktionsdefinitionen!! Compiler-Aufruf: gcc -o matrix_test matrix_test.c -Imeine_header Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 14.06.2017 C-Präprozessor: Einfügungen Standard-Header Syntax: #include <systemheader.h> Fügt den Text der Headerdatei systemheader.h an der Stelle im Code ein, an welcher der #include-Befehl steht. Die Datei wird im Standard-Header-Pfad des Systems gesucht. Bei Unix-Systemen ist dies üblicherweise /usr/include. Beispiel: #include <math.h> bindet die Datei /usr/include/math.h ein. Bei Headern in Unterordnern muss dieser mit angegeben werden, z. B. #include <sys/time.h> für den Header /usr/include/sys/time.h. Lokale (eigene) Header Syntax: #include “lokaler_header.h” Bindet die Datei lokaler_header.h ein, die im aktuellen Verzeichnis liegt. Will man einen Header aus einem anderen Pfad inkludieren, muss dem Compiler der Pfad mitgeteilt werden. Beispiel: Zum Einbinden des Headers matrix.h im Unterordner meine_header wird im Quellcode die Zeile #include “matrix.h” eingefügt. der Compiler mit der Option -Imeine_header aufgerufen. Die Option -IOrdner macht die Header in Ordner für den Compiler sichtbar. C-Präprozessor: Einfügungen – Beispiel matrix.h Headerdatei matrix.h im Unterordner meine_header (Ausschnitt): 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 // Legt Speicher fuer eine neue Matrix an und liefert einen Zeiger // auf die erste Zeile; bei Misserfolg NULL double **neueMatrix(int n_zeilen, int n_spalten); 7 8 9 // Gibt die Matrix am Bildschirm aus void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten); Programmcode matrix_test.c: 1 #include "matrix.h" 2 3 4 5 6 7 8 int main(void) { int n = 4, m = 7; double **H = neueMatrixAusBinDatei("Matrix.bin", n, m); zeigeMatrix(H, n, m); loescheMatrix(H, n); 9 10 11 return 0; } // Hiernach die Funktionsdefinitionen!! Compiler-Aufruf: gcc -o matrix_test matrix_test.c -Imeine_header C-Präprozessor: Makros ohne Parameter I Syntax: #define MAKRO_OHNE_PARAMETER Definiert ein Makro mit dem Namen MAKRO_OHNE_PARAMETER Namenskonvention: Makros werden immer durchgehend mit Großbuchtstaben benannt. Mit #ifdef MAKRO_OHNE_PARAMETER bzw. #ifndef MAKRO_OHNE_PARAMETER kann abgefragt werden, ob das Makro definiert bzw. nicht definiert ist. Syntax: #ifdef MAKRO // Weitere Direktiven, z. B. #include #endif Verwendungsbeispiel: Verhindern, dass ein Header mehrfach eingebunden wird. #ifndef __MEIN_HEADER_H__ #define __MEIN_HEADER_H__ // Weitere Direktiven, Funktionsdeklarationen etc. #endif C-Präprozessor: Makros ohne Parameter II Syntax: #define MAKRO Ersetzungstext Bewirkt, dass überall im Quellcode, wo MAKRO steht, der Ersetzungstext eingesetzt wird. Beispiele: #define ANTWORT 42 Alle Vorkommen von ANTWORT im Quellcode wird textuell durch 42 ersetzt. Auf diese Weise brauchen für Konstanten keine Variablen deklariert zu werden. #define ADRESSE long unsigned int In der Folge kann ADRESSE wie ein Datentyp verwendet werden. So lassen sich intuitive Namen für Datentypen vergeben. Generelle Verwendung: Vergabe von sprechenden Namen für Konstanten, Datentypen, . . . . Vermeidung von „magische Zahlen“ (PUFFERGROESSE hat mehr Aussagekraft als 1024). Durch Änderung der Definition lassen sich alle Vorkommen der Konstanten schnell und unkompliziert anpassen. Folge: Besser lesbarer und leichter zu pflegender Code C-Präprozessor: Makros mit Parametern Syntax: #define MAKRO(Parameterliste) Ersetzungstext Im Code lässt sich MAKRO wie eine Funktion aufrufen. Achtung: Es wird nicht überprüft, ob die Argumente passende Typen haben. Generell umfassen Makros nur eine einzige Zeile. Lange Zeilen lassen sich mit einem Backslash \ am Ende umbrechen. Beispiel 1: #define MAX(X,Y) X > Y ? X : Y Berechnet das Maximum von zwei Argumenten Beispiel 2: #define TAUSCHE(X,Y) int z = X; X = Y; Y = z; Vertauscht die Werte von zwei int-Argumenten. C-Präprozessor: Makros mit variabler Anzahl von Parametern Syntax: #define MAKRO(Parameterliste,...) Ersetzungstext Werden in Verbindung mit Funktionen verwendet, die mit einer variablen Anzahl von Argumenten aufgerufen werden können, z. B. fprintf. Das oben definierte Makro muss mit mindestens so vielen Parametern aufgerufen werden wie die Parameterliste lang ist. Auf die über diese Anzahl hinausgehenden Parameter, die anstelle der Punkte ... übergeben werden, kann mit dem Makro __VA_ARGS__ zugegriffen werden. Beispiel: #define EPRINTF(_fmt_str,...) \ fprintf(stderr, _fmt_str, ##__VA_ARGS__) Der Aufruf EPRINTF(“i = %d\n”, i); wird beispielsweise ersetzt durch fprintf(stderr, “i = %d\n”, i); Die doppelte Raute ## vor __VA_ARGS__ bewirkt hier speziell, dass das Komma nach dem Formatstring wegfällt, falls __VA_ARGS__ leer ist. Andernfalls ergäbe sich ein Syntaxfehler. Beispiel: EPRINTF(“Hallo!\n”); wird hier ersetzt zum syntaktisch korrekten fprintf(stderr, “Hallo!\n”); Ohne die Doppelraute würde die Ersetzung fprintf(stderr, “Hallo!\n”, ); lauten, was einen Syntaxfehler darstellt. C-Präprozessor: Fallstricke Argumente sollten im Ersetzungstext immer geklammert werden, da sonst zusammengesetzte Ausdrücke als Argument eventuell falsch ausgewertet werden. Beispiel: #define ZWEIMAL(X) 2 * X Der Aufruf ZWEIMAL(4 + 2) wird aufgelöst zu 2 * 4 + 2, im Ergebnis 10. Das Makro #define ZWEIMAL(X) 2 * (X) liefert das korrekte Ergebnis: 2*(4 + 2)=12. Formeln sollten im Ersetzungstext immer geklammert werden, da sonst zusammengesetzte Ausdrücke als Argument eventuell falsch ausgewertet werden. Beispiel: #define MAX(X,Y) (X) > (Y) ? (X) : (Y) Der Aufruf 2*MAX(3,5) wird aufgelöst zu 2 * (3) > (5) ? (3) : (5), im Ergebnis 3. Das Makro #define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) liefert das korrekte Ergebnis: 2*((3) > (4) ? (3) : (5))=10. C-Präprozessor: Fallstricke Anweisungsfolgen sind in geschweifte Klammern zu setzen. Beispiel 1: #define ABBRUCH_VOID printf(“Abbruch!\n”); return; Der Code if(fehler) ABBRUCH_VOID; resultiert im aufgelösten Code if(fehler) printf(“Abbruch!\n”); return; Dadurch wird immer das return-Statement ausgeführt! Mit #define ABBRUCH_VOID {printf(“Abbruch!\n”); return;} tritt dieses Problem nicht auf. Beispiel 2: #define TAUSCHE(X,Y) int z = X; X = Y; Y = z; Falls eine Variable z bereits deklariert wurde, beschwert sich der Compiler! Die Definition #define TAUSCHE(X,Y) {int z = X; X = Y; Y = z;} behebt diesen Konflikt, da die Deklaration im neuen Anweisungsblock übergeordnete Variablen bis zum Ende des Blocks überdecken kann. C-Präprozessor: Fallstricke Ausdrücke mit Nebeneffekten oder Aufrufe von rechenzeitintensiven Funktionen sollten in Makros vermieden werden, da sie eventuell mehrfach ausgewertet werden. Beispiel 1: char c = MAX(fgetc(stdin), ’a’); wird aufgelöst zu char c = ((fgetc(stdin)) > (’a’)) ? (fgetc(stdin)) : ’a’; Falls das erste eingelesene Zeichen (als Zahl) größer ist als ’a’, so wird ein zweites Zeichen eingelesen. Beispiel 2: int i = 5, j = MAX(i++, 2); wird aufgelöst zu int i = 5, j = ((i++) > (2)) ? (i++) : (2); Danach gilt nicht wie erwartet i = und j = , sondern i = Beispiel 3: und j = . double x = 1.0; double y = MAX(viel_zu_rechnen(x), 0.0); Hier wird unter Umständen die Funktion viel_zu_rechnen zweimal ausgewertet, was zu einer doppelt so langen Laufzeit führt! C-Präprozessor: Vor- und Nachteile von Makros gegenüber Funktionen Vorteile: Makros werden zur Compilezeit ausgewertet und sind in vielen Fällen schneller. Viele Makros können universell eingesetzt werden (z. B. MAX für alle vergleichbaren Datentypen). Nachteile: Zu viele oder zu lange Makros lassen den Codeumfang anwachsen und ergeben unter Umständen große und langsame Programme. Zudem ist der Speicherbedarf größer als bei Funktionen. Mehrfache Auswertung von Ausdrücken kann unerwünschte Konsequenzen nach sich ziehen. Es gibt keine Möglichkeit zu prüfen, ob sinnvolle Datentypen als Parameter verwendet werden. Die Fehlerquellen im Umgang mit Makros sind vielfältig. Der Versuch, Funktionspointer als Zeiger auf ein Makro zu verwenden, scheitert an einem Syntaxfehler. Dies ist vor allem dann problematisch, wenn nicht klar ist, ob ein Name für eine Funktion oder ein Makro steht. Fazit: Makros mit Parametern als Ersatz für Funktionen eignen sich für einfache Aufgaben in geschwindigkeitskritischen Bereichen. C-Präprozessor: bedingte Ersetzung Konditionale Direktiven: #if Bedingung1 // Direktiven / Code #elif Bedingung // Weitere Direktiven / Code #else // Alternative Direktiven / Code #endif Prinzipiell funktioniert dieses Konstrukt wie C-Statements mit if - else if - else (#elif ist eine Kurzform für „else if“). Als Bedingungen können konstante Zahlen, andere Makros sowie arithmetische und logische Ausdrücke verwendet werden. #ifdef MAKRO // bzw. #ifndef // Weitere Direktiven #endif Direktiven werden ausgeführt, falls MAKRO (nicht) definiert wurde. Wichtig: #if und #ifdef müssen immer durch ein #endif abgeschlossen werden. Definition rückgängig machen: #undef MAKRO C-Präprozessor: bedingte Ersetzung – Beispiel Header debug.h: 1 2 #ifndef __DEBUG_H__ #define __DEBUG_H__ 3 4 #include <stdio.h> 5 6 7 8 9 #ifdef DEBUG_AN #define DEBUG_AUSGABE(_fmt_string, ...) \ fprintf(stderr, "[Datei %s, Zeile %d] " _fmt_string, \ __FILE__, __LINE__, ##__VA_ARGS__) 10 11 12 #else #define DEBUG_AUSGABE(_fmt_string, ...) // definiert als "nichts" 13 14 15 #endif // DEBUG_AN #endif // __DEBUG_H__ Programmcode debug_test.c: 1 #include "debug.h" 2 3 4 5 6 7 int main(void) { int i = 42; DEBUG_AUSGABE("Hallo Welt!\n"); DEBUG_AUSGABE("i = %d\n", i); 8 return 0; 9 10 } C-Präprozessor: bedingte Ersetzung – Beispiel Codeanalyse Header: #ifdef DEBUG_AN #define DEBUG_AUSGABE(_fmt_string, ...) \ fprintf(stderr, "[Datei %s, Zeile %d]: " _fmt_string, \ __FILE__, __LINE__, ##__VA_ARGS__) #else #define DEBUG_AUSGABE(_fmt_string, ...) // definiert als "nichts" #endif // DEBUG_AN Falls DEBUG_AN definiert wurde, nimmt der Präprozessor die obere Version des Makros DEBUG_AUSGABE, andernfalls die untere leere Version. Die hintereinander geschriebenen Strings “[Datei %s, Zeile %d]: ” und _fmt_string werden automatisch zu einem zusammengefügt (konkateniert). Dies gilt generell für konstante Strings in C. __FILE__ und __LINE__ sind vordefinierte Makros, die vom Präprozessor durch den Dateinamen (als String) und die Codezeile (als Ganzzahlkonstante) ersetzt werden. Aus diesem Grund sind dafür die Platzhalter %s bzw. %d vorgesehen. Wurde DEBUG_AN nicht definiert, so ersetzt der Präprozessor alle Aufrufe von DEBUG_AUSGABE durch nichts. In diesem Fall wäre beispielsweise DEBUG_AUSGABE(“Hallo!\n”); ein leeres Statement (;). C-Präprozessor: bedingte Ersetzung – Beispiel Codeanalyse Hauptprogramm: int i = 42; DEBUG_AUSGABE("Hallo Welt!\n"); DEBUG_AUSGABE("i = %d\n", i); Compileraufruf 1: gcc -o debug_test debug_test.c Das Makro DEBUG_AN ist nicht definiert, daher erfolgt keine Ausgabe. Compileraufruf 2: gcc -o debug_test debug_test.c -DDEBUG_AN Durch die Option -DDEBUG_AN wird das Makro DEBUG_AN definiert, und der Präprozessor verwendet die „gehaltvolle“ Version von DEBUG_AUSGABE. Das zweite DEBUG_AUSGABE-Statement in obigem Code beispielsweise wird wie erwartet durch fprintf(stderr, “[Datei %s, Zeile %d] ” “i = %d\n”, i); ersetzt. Generell wirkt die Compileroption -DMAKRO[=Wert] wie eine Zeile #define MAKRO [Wert] im Code. Achtung: Bei Stringkonstanten müssen Zeichen, die normalerweise von der Kommandozeile interpretiert werden, mit einem Backslash „escaped“ werden. Dazu gehören u. a. Anführungszeichen, Hochkommata, Backslash, Dollar, Raute, Klammern, . . . Beispiel: Um ein Makro NACHRICHT mit dem Wert “Im Westen nichts Neues.” zu definieren, muss die Compileroption -DNACHRICHT=\“Im Westen nichts Neues.\” lauten. C-Präprozessor: mehrstufige Ersetzungen und Zugriff auf Variablennamen Es ist möglich, Makros zu definieren, die von anderen Makros abhängen. Beispiel: Kurzform eines langen Konstantennamens #define ERDBESCHLEUNIGUNG 9.81 #define G ERDBESCHLEUNIGUNG Generell durchläuft der Präprozessor den Code so lange, bis alle Ersetzungen erfolgt sind. Beim ersten Durchlauf werden alle Vorkommen von G durch ERDBESCHLEUNIGUNG ersetzt. Im zweiten Durchgang erfolgt die endgültige Auflösung zur Konstanten 9.81. Es ist möglich, mit Makros auf den Namen von Variablen zuzugreifen. Beispiel: #define PRINTDOUBLEWITHNAME(X) printf("%s = %.3lf ",#X,X) Im Programm führt der Aufruf double x=1.121,y=2.3; PRINTDOUBLEWITHNAME(x); PRINTDOUBLEWITHNAME(y); dann zu folgender Ausgabe: x = 1.121 y = 2.300. Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 21.06.2017 Programmierprojekte mit mehreren Dateien Die Präprozessordirektive #include “eigener_header.h” erlaubt es, Funktionsdeklarationen, typedefs, Definitionen von Makros usw. in einen externen Header auszulagern. Nach wie vor müssen jedoch alle Funktionsdefinitionen im Hauptprogrammcode enthalten sein. Diese Tatsache führt zu langen und unübersichtlichen Codedateien und verhindert eine einfache Wiederverwendung der programmierten Funktionen (einzige Möglichkeit: copy&paste in andere Projekte). Wünschenswert wäre eine Möglichkeit, Funktionsdefinitionen (gruppiert nach Aufgabengebiet) in externe Quellcode-Dateien auszulagern. Mit Hilfe von Objektdateien, einem Zwischenprodukt beim Compilieren von Code, lässt sich diese Möglichkeit realisieren. Programmierprojekte mit mehreren Dateien: Prinzip main.c modul_1.c ... modul_N.c gcc -c -o <ausgabe>.o <code>.c main.o modul_1.o ... modul_N.o Bibliotheken gcc -o programm main.o modul_1.o [...] modul_N.o -lBibliotheksname programm Programmierprojekte mit mehreren Dateien: Prinzip Der Quellcode ist verteilt auf mehrere Module - getrennt nach Funktionalität und das Hauptprogramm. Jede Codedatei wird separat zu Objektcode compiliert mit dem Aufruf gcc -c -o Datei.o Datei.c Mit gcc -o programm Liste_von_Objektdateien werden die Objektdateien zu einem ausführbaren Programm „zusammengebunden“ (gelinkt). Erinnerung: Der Compiler braucht alle nötigen Deklarationen und gibt als Resultat Objektcode aus. Erst der Linker muss die Definitionen zur Verfügung haben, sonst liefert er einen undefined reference-Fehler. Das Ergebnis des Link-Vorgangs ist schließlich das ausführbare Programm. Programmierprojekte mit mehreren Dateien: Beispiel – matrix.h 1 2 #ifndef __MATRIX_H__ #define __MATRIX_H__ 3 4 5 6 // Legt Speicher fuer eine neue Matrix an und liefert einen Zeiger auf die // erste Zeile; bei Misserfolg NULL double **neueMatrix(int n_zeilen, int n_spalten); 7 8 9 // Gibt die Matrix am Bildschirm aus void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten); 10 11 12 // Gibt den Speicherplatz der Matrix frei void loescheMatrix(double **matrix, int n_zeilen); 13 14 15 16 // Reserviert Speicher fuer eine Matrix und liest die Elemente aus der // TEXTdatei, die unter dem Pfad zu finden ist; bei Misserfolg NULL double **neueMatrixAusDatei(char *pfad, int *n_zeilen, int *n_spalten); 17 18 19 20 // Schreibt die Matrix im Textmodus in die Datei im angegebenen Pfad void schreibeMatrixInDatei(char *pfad, double **matrix, int n_zeilen, int n_spalten); 21 22 23 24 // Reserviert Speicher fuer eine Matrix und liest die Elemente aus der // BINAERdatei, die unter dem Pfad zu finden ist; bei Misserfolg NULL double **neueMatrixAusBinDatei(char *pfad, int n_zeilen, int n_spalten); 25 26 27 28 29 // Schreibt die Matrix im Binaermodus in die Datei im angegebenen Pfad void schreibeMatrixInBinDatei(char *pfad, double **matrix, int n_zeilen, int n_spalten); #endif Programmierprojekte mit mehreren Dateien: Beispiel – matrix_funktionen.c 1 2 #include <stdio.h> #include <stdlib.h> 3 4 5 6 7 double **neueMatrix(int n_zeilen, int n_spalten) { int i, j; double **matrix; 8 matrix = (double **)malloc(n_zeilen * sizeof(double *)); if (matrix == NULL) return NULL; 9 10 11 12 for (i = 0; i < n_zeilen; i++) { matrix[i] = (double *)malloc(n_spalten * sizeof(double)); if (matrix[i] == NULL) { for (j = 0; j < i; j++) free(matrix[j]); free(matrix); return NULL; } } return matrix; 13 14 15 16 17 18 19 20 21 22 23 24 25 } 26 27 // Und alle weiteren Funktionsdefinitionen Programmierprojekte mit mehreren Dateien: Beispiel – matrix_test_main.c 1 #include "matrix.h" 2 3 4 5 6 int main(void) { int n, m; double **G, **H; 7 G = neueMatrixAusDatei("Matrix_kommentiert.dat", &n, &m); H = neueMatrixAusBinDatei("Matrix.bin", n, m); 8 9 10 zeigeMatrix(G, n, m); zeigeMatrix(H, n, m); 11 12 13 loescheMatrix(G, n); loescheMatrix(H, n); 14 15 16 return 0; 17 18 } Kommandos: gcc -c -o matrix_test_main.o matrix_test_main.c (Compiler) gcc -c -o matrix_funktionen.o matrix_funktionen.c (Compiler) gcc -o matrix_test matrix_test_main.o matrix_funktionen.o (Linker) Programmierprojekte mit mehreren Dateien Anmerkungen: Das Modul matrix_funktionen.c benötigt die Systemheader stdio.h und stdlib.h, damit die dort deklarierten Funktionen (z. B. printf oder malloc) verwendet werden können. Sind sie nicht eingebunden, liefert der Compiler (gcc -c) einen Fehler. Der Header matrix.h braucht hingegen nicht inkludiert zu werden. Im Hauptprogramm genügt es, matrix.h einzufügen. Gäbe es Statements, die beispielsweise printf enthalten, so müsste zusätzlich stdio.h inkludiert werden. Merkregel: Jede Codedatei benötigt genau diejenigen Header, die nötig sind, damit alle Variablen, Konstanten, Funktionen, . . . innerhalb dieser Datei korrekt deklariert sind. Wird der Code eines Moduls geändert, so müssen alle Objekte und Programme neu compiliert und gelinkt werden, die von diesem Modul abhängen. In diesem Beispiel müssen nach einer Änderung in matrix_funktionen.c alle Schritte von neuem durchgeführt werden! Bei größeren Projekten mit vielen Dateien wird es zunehmend schwierig nachzuvollziehen, welche Aktionen nach welcher Änderung vollzogen werden müssen. Hierfür gibt es die elegante Lösung der Makefiles. Mehrdateiprojekte mit make make ist das mit Abstand wichtigste Entwicklungs-Tool bei Softwareprojekten. Wir betrachten hier die am weitesten verbreitete Implementierung GNU Make (www.gnu.org/software/make/) Aufgabe: Neuübersetzung genau derjenigen Programmteile, die von einer Änderung am Code betroffen sind. Hintergrund: Die „Brechstangen“-Methode, bei jeder Änderung alles neu zu übersetzen, ist aus zwei Gründen nicht praktikabel: 1 Umfangreiche Projekte benötigen Minuten bis Stunden zur Komplettübersetzung 2 Für jede einzelne Datei gcc aufzurufen (mit individuellen Optionen) ist extrem mühsam. Funktionsweise: In einer Steuerungsdatei mit dem Namen Makefile gibt man alle Abhängigkeiten zwischen den Dateien in Form von Erstellungsregeln (engl. make rules) an. Mehrdateiprojekte mit make: Erstellungsregeln Aufbau: Ziel: Abhaengigkeiten \ weitere Abhaengigkeiten −−−−−→Befehl1 −−−−−→... −−−−−→BefehlN Ziel bezeichnet „das, was getan werden soll“, also entweder einen Dateinamen (z. B. matrix_funktionen.o) oder eine abstrakte Aktion (z. B. clean). Als Abhängigkeiten werden sämtliche Dateien angegeben, von deren Änderung das Ziel abhängt bzw. abhängen soll. Lange Zeilen können mit einem Backslash am Ende umgebrochen werden. Die Befehle definieren, was make unternehmen soll, um das Ziel zu erstellen. Wichtig: Befehlszeilen müssen immer mit einem Tabulator (TAB-Taste) beginnen, sonst meldet make einen Fehler. Mehrdateiprojekte mit make: Beispiel Matrix-Projekt Bisheriger Inhalt: matrix.h, matrix_funktionen.c und matrix_test_main.c Jede Quellcodedatei .c wird zu einer Objektdatei .o kompiliert. Diese Objekte werden schließlich zu einem ausführbaren Programm zusammengebunden. Abhängigkeiten: matrix_funktionen.o: Entsteht aus matrix_funktionen.c matrix_test_main.o: Entsteht aus matrix_test_main.c und inkludiert matrix.h matrix_test: Entsteht aus matrix_test_main.o und matrix_funktionen.o Daraus ergeben sich folgende Erstellungsregeln: 1 2 matrix_test: matrix_test_main.o matrix_funktionen.o −−−−−→gcc -o matrix_test matrix_test_main.o matrix_funktionen.o 3 4 5 matrix_test_main.o: matrix_test_main.c matrix.h −−−−−→gcc -c -o matrix_test_main.o matrix_test_main.c 6 7 8 matrix_funktionen.o: matrix_funktionen.c −−−−−→gcc -c -o matrix_funktionen.o matrix_funktionen.c Von nun an genügt es, nach jeder Änderung auf der Kommandozeile make aufzurufen: $ make gcc -c -o matrix_test_main.o matrix_test_main.c gcc -c -o matrix_funktionen.o matrix_funktionen.c gcc -o matrix_test matrix_test_main.o matrix_funktionen.o Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“ Z = Ziel im Makefile = Abhängigkeit neuer als Ziel = nicht vorhanden A B = erstellt und aktuell C = kein Ziel a D b A c E c d Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“ Z = Ziel im Makefile = Abhängigkeit neuer als Ziel = nicht vorhanden A B = erstellt und aktuell C = kein Ziel a D b A c E c d Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“ Z = Ziel im Makefile = Abhängigkeit neuer als Ziel = nicht vorhanden A B = erstellt und aktuell C = kein Ziel a D b A c E c d Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“ Z = Ziel im Makefile = Abhängigkeit neuer als Ziel = nicht vorhanden A B = erstellt und aktuell C = kein Ziel a D b A c E c d Auflösung der Abhängigkeiten: Nach Aktualisierung der Datei „a“ Z = Ziel im Makefile = Abhängigkeit neuer als Ziel = nicht vorhanden A B = erstellt und aktuell C = kein Ziel a D b A c E c d Auflösung der Abhängigkeiten: Nach Aktualisierung der Datei „a“ Z = Ziel im Makefile = Abhängigkeit neuer als Ziel = nicht vorhanden A B = erstellt und aktuell C = kein Ziel a D b A c E c d Auflösung der Abhängigkeiten: Nach Aktualisierung der Datei „a“ Z = Ziel im Makefile = Abhängigkeit neuer als Ziel = nicht vorhanden A B = erstellt und aktuell C = kein Ziel a D b A c E c d Mehrdateiprojekte mit make Bemerkungen: Das Kommando make Ziel erstellt Ziel anhand der Abhängigkeiten in der Datei Makefile. Der Befehl make ohne Argument erstellt das erste Ziel im Makefile. Eine Erstellungsregel muss nicht unbedingt Abhängigkeiten besitzen. Beispiel: Regel zum Aufräumen clean: −−−−−→rm *.o matrix_test -f Mit make clean können nun alle Sicherungs- und Objektdateien sowie das Programm gelöscht werden. Auch Befehle müssen nicht zwingend in einer Regel enthalten sein. Beispiel: Alles erstellen all: matrix_test matrix_test_main.o matrix_funktionen.o Durch die Auflistung sämtlicher Objekt- und Programmdateien als Abhängigkeiten von all lässt sich mit dem Aufruf make all das gesamte Projekt aktualisieren. Mehrdateiprojekte mit make: implizite Regeln und Variablen GNU Make verfügt über eine große Menge von impliziten Regeln, die im Makefile nicht neu definiert werden müssen. Wichtigstes Beispiel: %.o: %.c −−−−−→$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ In Worten: Jedes Objekt Datei.o hängt vom entsprechenden Code Datei.c ab. Zur Erstellung eines solchen Objekts ist folgender Befehl aufzurufen: Compiler -c Compileroptionen Präprozessoroptionen Datei.c -o Datei.o Zusätzliche Abhängigkeiten können als Regel ohne Befehl angegeben werden. Beispiel: matrix_test_main.o: matrix.h Mit Hilfe der Variablen CC, CFLAGS und CPPFLAGS lassen sich Compilername und Optionen anpassen. Beispiel: CC = gcc CFLAGS = -Wall CPPFLAGS = -DDEBUG_AN resultiert im generellen Compilerbefehl gcc -c -Wall -DDEBUG_AN Datei.c -o Datei.o Mehrdateiprojekte mit make: Vereinfachtes Matrix-Makefile Ursprüngliches explizites Makefile: 1 2 matrix_test: matrix_test_main.o matrix_funktionen.o −−−−−→gcc -Wall -o matrix_test matrix_test_main.o matrix_funktionen.o 3 4 5 matrix_test_main.o: matrix_test_main.c matrix.h −−−−−→gcc -c -Wall -o matrix_test_main.o matrix_test_main.c 6 7 8 matrix_funktionen.o: matrix_funktionen.c −−−−−→gcc -c -Wall -o matrix_funktionen.o matrix_funktionen.c Angepasstes Makefile: 1 2 CC = gcc CFLAGS = -Wall 3 4 5 matrix_test: matrix_test_main.o matrix_funktionen.o −−−−−→$(CC) $(CFLAGS) $^ -o $@ 6 7 matrix_test_main.o: matrix.h Mehrdateiprojekte mit make: Vereinfachtes Matrix-Makefile Bemerkung: Wichtige vordefinierte Variablen sind beispielsweise: $@ Name des Ziels $ˆ Liste aller Abhängigkeiten ohne Wiederholungen $+ Liste aller Abhängigkeiten $< erste Abhängigkeit Resultat des angepassten Makefiles: $ make gcc -Wall -c -o matrix_test_main.o matrix_test_main.c gcc -Wall -c -o matrix_funktionen.o matrix_funktionen.c gcc -Wall matrix_test_main.o matrix_funktionen.o -o matrix_test Programmierung für Mathematiker Prof. Dr. Thomas Schuster M.Sc. Dipl.-Phys. Anne Wald 28.06.2017 und 05.07.2017 Die kommerzielle Software MATLAB von MathWorks MathWorks TAH Campuslizenz Die Software darf von allen Studierenden der Universität des Saarlandes genutzt werden. Das umfasst einerseits beliebig viele Installationen auf dem Campus und andererseits auch die Nutzung auf privaten Computern. Notwendig: Registrierung bei der Firma ASKnet AG. Weitere Informationen: https://unisb.asknet.de/cgi-bin/program/S1552 Weitere Informationen: http://www.hiz-saarland.de/informationen/arbeitsplatz/sw-lizenzen/ Herstellerseite: http://www.mathworks.com http://www.mathworks.de Einleitung Matlab Matlab (Matrix Laboratory ) ist ein interaktives Matrix-orientiertes Softwaresystem zur Berechnung und Lösung numerischer Probleme. Warum Matlab? benutzerfreundliche Syntax umfangreiche Sammlung mathematischer Algorithmen vielfältige und einfach realisierbare Datenausgabe / Visualisierung kurze Entwicklungszeiten, in der eingebauten Programmiersprache lassen sich Algorithmen schnell und leicht realisieren (Interpretersprache) einfaches Debugging hohe Absturzsicherheit Nachteile: etwas höhere Anforderungen an den Rechner (v.a. Speicherbedarf) langsamer als kompilierter Code, insbesondere bei for-Schleifen Aber: Programme anderer Sprachen, z.B. C, lassen sich leicht einbinden. Literatur Anne Angermann et al.: Matlab-Simulink-Stateflow: Grundlagen, Toolboxen, Beispiele, Oldenbourg Verlag, 2011 Wolfgang Schweizer: Matlab kompakt, Oldenbourg Verlag, 2009 Cleve Moler: Numerical Computing with Matlab, SIAM, 2010 Stormy Attaway: MATLAB: A Practical Introduction to Programming and Problem Solving, Butterworth Heinemann, 2011 Günter Gramlich: Eine Einführung in Matlab aus der Sicht eines Mathematikers www.hs-ulm.de//users/gramlich/EinfMATLAB.pdf Susanne Teschl: MATLAB - Eine Einführung staff.technikum-wien.at/~teschl/MatlabSkriptum.pdf weitere Informationen: Homepage von Matlab: www.mathworks.de bzw. www.mathworks.com Informationen zur Campuslizenz: www.hiz-saarland.de/informationen/arbeitsplatz/sw-lizenzen/mathworks-tah-campuslizenz/ unisb.asknet.de/cgi-bin/program/S1552 Matlab-Oberfläche aktuelles Release: R2012b Matlab-Oberfläche Starten mit $ matlab & Elemente der Matlab-Oberfläche: Command Window: direkte Eingabe von Matlab-Befehlen Command History: Historie der im Command Window eingegebenen Befehle Workspace Browser: Anzeige der Variablen des Base-Speicherbereichs Current Folder: Auflistung der Dateien des aktuellen Verzeichnisses Editor: Bearbeitung von Matlab-Skripten und Funktionen (mit Syntax-Highlighting, Tab-Vervollständigung, ...) erste Schritte in Matlab Eingabe direkt im Command Window Ausgabe erscheint beim Weglassen des Semikolons am Ende der Befehlszeile. Es gilt „Punktrechnung vor Strichrechnung“. Es wird zwischen Klein- und Großbuchstaben unterschieden. Strings werden durch Hochkommata erzeugt. Beispiele: >> 2.5+1.5 ans = 4 >> c=a^2+b^2 c = 25 >> a=3; >> b=4; >> a/b ans = 0.75 >> d=a+b*c d = 103 >> s = 'Hallo' Unterschied zu C: keine Deklaration der Variablen nötig! Variablen werden (intern) automatisch als double behandelt und belegen 8 Bytes Speicherplatz. ; kein cast notwendig! Ganzzahlige Ergebnisse werden als ganze Zahl ausgegeben. Script-Files Skripte / Script-Files Ein Skript ist eine Folge von Anweisungen, die im Editor eingegeben und mit der Endung „.m“ abgespeichert werden. Es wird durch Eingabe des Dateinamens im Command Window oder durch den Button „Run“ im Editor (alternativ: F5) ausgeführt, d.h. alle Anweisungen werden der Reihe nach abgearbeitet. Beispiel: Das Skript testskript.m a=3; b=4; c=a^2+b^2 führt nach Ausführung zu der Ausgabe c=25 auf dem Bildschirm. Zudem sind die Variablen a=3, b=4 und c=25 im Workspace enthalten (und können bei Bedarf für weitere Berechnungen verwendet werden). Operatoren Vergleichende und logische Operatoren wie in C Ausnahme: Notation in Matlab Notation in C a < b a >= b a == b A && B A || B a ∼= b ∼A a < b a >= b a == b A && B A || B a != b !A Unterschied zu C: In Matlab dürfen a und b Matrizen gleicher Dimension sein. In diesem Fall wird elementweise verglichen und eine Matrix gleicher Dimension mit Nullen und Einsen zurückgegeben. keine Inkrement- und Dekrementoperatoren a++, a−− keine arithmetischen Zuweisungsoperatoren a+=2 Ausgewählte Variablen und Konstanten ans wird dem Ergebnis kein Variablenname zugeordnet, so wird automatisch die Variable ans erzeugt inf liegt das Ergebnis nicht im Intervall [-realmax; realmax] erhält dieses automatisch den Wert inf NaN Ergebnis nicht definierter arithmetischer Operationen, z.B. 0/0, inf/inf, 0*inf, sind vom Typ NaN (Not a Number) pi =π realmax größte positive Zahl (PC:1.797710308 ) realmin kleinste positive Zahl (PC: 2.225110−308 ) In Matlab ist es möglich mit komplexen Zahlen zu rechnen: i,j conj(Z) real(Z),imag(Z) angle(Z), abs(Z) imaginäre Einheit (i 2 = −1) konjugiert komplexe Zahl zu Z Real- bzw. Imaginärteil von Z Polardarstellung von Z Nützliche Befehle bzw. Funktionen clc clear [mod] ctrl + c help [Name] löscht die Oberfläche des Command Window, aber nicht die Variablen selbst löscht alle Variablen aus dem Workspace mod = var: nur die Variable var wird gelöscht mod = all: alle Variablen werden gelöscht (Achtung: auch globale!) „Notbremse“, bricht die aktuelle Berechnung ab listet alle bzw. die zu Name gehörigen Hilfe-Themen auf, bequemer: GUI (Grafische Benutzeroberfläche) benutzen (z. B. mit F1) isinf(var) liefert 1, falls var vom Typ inf isnan(var) liefert 1, falls var vom Typ NaN who whos listet die im Workspace vorhandenen Variablen auf liefert eine detailierte Liste der Variablen (Typ, Wert) Erzeugen von Matrizen Eingabe in eckigen Klammern: einzelne Komponenten in einer Zeile werden durch Leerzeichen oder Komma getrennt neue Zeilen werden durch ein Semikolon gekennzeichnet x = [1 2 3] A = [1 2 3; 4 5 6] erzeugt den Zeilenvektor x = (1, 2, 3) sowie die Matrix A = 1 4 2 5 3 . 6 Merke: Zeilenvektoren werden intern als 1 × N-Matrizen behandelt. Eingabe durch Steuerung der Schrittweite: y= a:h:b erzeugt einen Vektor von a bis b mit Schrittweite h y= 0:2:6 erzeugt den Zeilenvektor y = (0, 2, 4, 6) Eingabe mit der Funktion linspace(a,b,N) erzeugt einen Vektor von a bis b mit N Komponenten y=linspace(0,6,4) erzeugt ebenfalls den Zeilenvektor y = (0, 2, 4, 6). Bemerkung: Bei großen Dimensionen ist die Eingabe mittels a:h:b etwas schneller als bei der Verwendung von linspace. Arbeiten mit Matrizen Die Indizierung von Matrizen beginnt in Matlab bei 1! Reservierung von Speicherplatz erfolgt automatisch. Initialisierung trotzdem sinnvoll, führt in der Regel zu schnelleren Programmen. Wir betrachten die Matrix A = 1 4 2 5 3 6 = a11 a21 a12 a22 a13 . a23 einzelnes Matrizenelement : A(i,j) liefert das Element ai,j >> A(2, 3) ans = 6 i-te Zeile einer Matrix: A(i,:) >> A(1,:) ans = 1 2 3 j-te Spalte einer Matrix: A(:,j) >> A(:,2) ans = 2 5 Teilmatrix: A(p:q,[r,s]) liefert die Zeilen p bis q und die Spalten r und s der Matrix >> A(1:2,[1,3]) ans = 1 3 4 6 Arbeiten mit Matrizen Wir betrachten zusätzlich die Matrix B = 1 0 2 . 3 B=[1,2;0,3] Addition, Subtraktion und Multiplikation definiert wie in Linearer Algebra >> B*A ans = 9 12 12 15 15 18 >> B+3 % oder 3+B ans = 4 5 3 6 elementweiser Zugriff auf Matrizen mit Punktoperator: >> B.^2 % oder B.*B ans = 1 4 0 9 Transponierte Matrix: >> B' ans = 1 0 2 3 >> B^2 % oder B*B ans = 1 8 0 9 Matrix-Funktionen det(B) Determinante von B eig(B) Eigenwerte und (normierte) Eigenvektoren von B lambda=eig(B) liefert λ = 1 3 [v,lambda]=eig(B) liefert v = rank(B) norm(B,1) norm(B,’fro’) inv(B) 1 0 √ −1 2 1 √ −1 und λ = 0 2 Rang der Matrix B 1-Norm (Spaltensummenorm) der Matrix B, entspricht dem Pn |b |, hier kBk1 = 5. Ausdruck max1≤j≤n i=1 ij Frobeniusnorm Matrix B, entspricht dem Ausdruck qP derP √ m n kBkF = |b |2 , hier kBkF = 14. i=1 j=1 ij Inverse der Matrix B, Ergebnis: B −1 = 1 3 3 0 −2 −1 Hinweis: Die explizite Berechnung einer inversen Matrix, um ein lineares Gleichungssystem zu lösen, ist in der Praxis (fast immer) zu aufwendig! Ausweg: Vorlesung Praktische Mathematik (QR-Zerlegung, . . . ) Matlab: linsolve 0 3 Matrix-Funktionen end maximaler Indexwert von Matrizen, z. B. liefert y(end) den letzten Eintrag des Vektors y . length höchste Dimension einer Matrix size beide Dimensionen einer Matrix zeros(M,N) M × N-Matrix, deren Einträge 0 sind ones(M,N) M × N-Matrix, deren Einträge 1 sind eyes(M) rand(M,N) A(:) M × M-Einheitsmatrix M × N-Zufallsmatrix, wobei jeder Matrixeintrag eine Realisierung einer auf [0,1)-gleichverteilten Zufallsvariablen ist. Vektor, der die Spalten der Matrix A hintereinandergereit enthält. Mit der Matrix A der vorigen Folie entspricht A(:) dem Vektor [1; 4; 2; 5; 3; 6]. A(A>2) Vektor, der die Spalten der Matrix A hintereinandergereit enthält, wobei nur Matrixelemente größer zwei berücksichtigt werden. Mit der Matrix A der vorigen Folie entspricht A(A>2) dem Vektor [4; 5; 3; 6]. max(v) kleinster Eintrag des Vektors v min(v) größter Eintrag des Vektors v sum(v) Summe der Vektorelemente Mathematische Funktionen Trigonometrische Funktionen: cos, sin, tan, asin, acos, atan, atan2 Soll die Berechnung in Grad durchgeführt werden, so müssen die Funktionen mit ’d’ ergänzt werden. Beispiel: >>sin(pi/4) ans = 0.7071 >> sind(45) ans = 0.7071 Exponential- und logarithmische Funktionen: exp, log, log10 Wurzeln: sqrt, nthroot(x,n) Rundungsfunktionen: ceil, floor, round (runden zum nächsten integer-Wert) Betragsfunktion: abs Hinweis: Diese Funktionen akzeptieren als Argumente Matrizen, die dann elementweise ausgewertet werden. Dies führt in der Regel zu schnellerem Code. if-Anweisung Syntax in Matlab if Bedingung_1 Anweisungsblock_1 elseif Bedingung_2 Anweisungsblock_2 . . . elseif Bedingung_N Anweisungsblock_N else Anweisungsblock end Syntax in C if (Bedingung_1) Anweisungsblock_1 else if (Bedingung_2) Anweisungsblock_2 . . . else if (Bedingung_N) Anweisungsblock_N else Anweisungsblock switch-Anweisung Syntax in Matlab switch Variable case Wert_1 Anweisungsblock_1 case Wert_2 Anweisungsblock_2 Syntax in C switch (Variable) { case Wert_1: Anweisungsblock_1 break; // optional case Wert_2: Anweisungsblock_2 . . . case Wert_N Anweisungsblock_N otherwise Anweisungsblock end . . . } case Wert_N: Anweisungsblock_N default: // optional Anweisungsblock // optional Unterschied zu C: In Matlab ist kein break notwendig, es wird immer nur der zum case gehörige Anweisungsblock ausgeführt. for- und while-Schleifen Syntax in Matlab Syntax in C for Variable = Vektor Anweisungsblock end for (Initialisierung; Bedingung; Update) Anweisungsblock while Bedingung Anweisungsblock; end while (Bedingung) Anweisungsblock Do-while-Schleifen existieren in Matlab nicht! (Ausweg: while 1 und break) Beispiel: x=1:1:10; summe=0; for k=1:10 summe=summe+x(k); end % Ergebnis entspricht sum(x) m=0; Erg=0; while m<=10 Erg=Erg+2; m=m+1; end Ein- und Ausgabe am Bildschirm Eingabe: variable=input(string) variable=input(string,’s’) Ausgabe von string auf dem Bildschirm, die Eingabe (Zahl, Variablenname) wird ausgewertet und variable zugewiesen. Ausgabe von string auf dem Bildschirm, die Eingabe wird nicht ausgewertet sondern als String variable zugewiesen. Ausgabe: disp(string) disp(Variable) fprintf(String, Parliste) Ausgabe von string auf dem Bildschirm. Ausgabe der Werte von variable auf dem Bildschirm. formatierte Ausgabe auf dem Bildschirm. Die Syntax entspricht im Wesentlichen der von printf in C. Matlab als Programmiersprache Neben den Script-Files gibt es noch Function-Files, welche auch in Dateien mit der Endung .m gespeichert werden. Sie beginnen mit dem Schlüsselwort function. function [ Rueckgabewerte ] = Funktionsname( Parameter ) % Beschreibung als Kommentar Anweisungsblock end Unterschiede zu C: beliebige Anzahl von Rückgabewerten Matrizen können übergeben werden Übergabemethode: shared-data-copy, d.h. intern werden Variablen, die nicht verändert werden, als Zeiger übergeben. Wird eine Variable in der Funktion verändert, so wird eine Kopie im Speicherbereich der Funktion erstellt. Funktionenvariablen sind lokale Variablen Function-Files unter Funktionsname.m abspeichern Aufruf anderer m-Files in m-Files möglich, sofern die Verzeichnisse in denen die Dateien liegen unter „Set Path“ eingetragen sind oder mit dem „Current Folder“ übereinstimmen. Nützliches tic-toc pause pause(n) nargin nargout (primitive) Zeitmessung: Mit tic wird die Stoppuhr auf null gesetzt, toc gibt die vergangene Zeit aus. wartet bis zu einem Tastendruck auf der Tastatur Pause für n Sekunden Anzahl der Funktionsparameter Anzahl der Rückgabewerte Beispiel: function [ erg ] = testfunktion(a, b) if nargin==1 erg=a; else erg=a+b; end %if end testfunktion(2) liefert das Ergebnis 2. testfunktion(2,3) liefert das Ergebnis 5. testfunktion(2,3,2) führt zu der Fehlermeldung: Too many input arguments Beispiel: Fakultät function [ erg ] = fakul(n) % berechnet n! = n*(n-1)*...*1 rekursiv if(n==0 || n==1) erg = 1; elseif n<0 disp('Bitte positive Zahl eingeben!') else erg = n*fak(n-1); end end function [ erg ] = fakul_iter(n) % berechnet n! = n*(n-1)*...*1 iterativ erg=1; for k=1:n erg=erg*k; end % Alternative zur for-Schleife: erg=prod(1:n); end Beispiel: Fakultät Startscript: % Vergleich Rekursion und Iteration k=20; % rekursiv tic n1=fakul(k); zeit1=toc; fprintf('\n Zeitverbrauch rekursiv: %f',zeit1); % iterativ tic n2=fakul_iter(k); zeit2=toc; fprintf('\n Zeitverbrauch iterativ: %f',zeit2); fprintf('\n Ergebnis rekursiv: %d, iterativ: %d \n',n1,n2); Ergebnis: Zeitverbrauch rekursiv: 0.000186 Zeitverbrauch iterativ: 0.000023 Ergebnis rekursiv: 2432902008176640000, iterativ: 2432902008176640000 Arbeiten mit Dateien spezielles Matlab-Binärformat (Endung: .mat) save(Dateiname, [Var1,...,VarN]) speichert die Variablen Var1 bis VarN (bzw. alle im Workspace enthaltenen) in die Datei Dateiname.mat load(Dateiname, [Var1,...,VarN]) lädt die Variablen Var1 bis VarN (bzw. alle enthaltenen) aus der Datei Dateiname.mat in den Workspace Textdateien fopen, fprintf, fscanf, fclose (nahezu) analog zu C, siehe Matlabhilfe für Details. Binärdateien fopen, fread, fwrite, fclose (nahezu) analog zu C, siehe Matlabhilfe für Details. verschiedene Dateiformate, z.B. xls, bmp, png, jpg, . . . und (einfache) Textdateien importdata(Dateiname,[Delimiter],[Kopfzeilen]) importiert zahlreiche Formate, wobei Delimiter das Spaltentrennzeichen festlegt und die Kopfzeilen beim Einlesen übersprungen werden Fallstricke Achtung: In Matlab ist fast alles erlaubt! Führt man das folgende Skript in Matlab aus, so erhält man keine Fehlermeldung und auch der Code Analyzer gibt keine Warnungen aus! sum = 0; for i = 1:10 sum = sum + i; end pi = 1; Folgende Probleme treten anschließend auf: 1 2 3 »sum([1 2]) Index exceeds matrix dimensions. Merke: Funktionen werden durch Variablen mit gleichem Namen überdeckt! »z=1+2*i z=21. Merke: Möchte man mit komplexen Zahlen arbeiten, so muss man auf i oder j als Laufindex für Schleifen verzichten! »sin(pi) ans=0.8415. Merke: Selbst Konstanten wie π können durch beliebige Werte überschrieben werden! Matlab-Profiler und Code Analyzer Code Analyzer Während der Eingabe wird eine Syntax-Prüfung des Codes durchgeführt, auffällige Stellen werden unterschlängelt. Zudem werden Syntax-Fehler mit roten und Warnungen durch orangefarbene Balken in der Scrollleiste markiert. mlintrpt mlintrpt(’Dateiname’) Anzeige des Code Analyzer Reports für alle Dateien im current folder. (alternativ Klick auf „Analyze Code“) Anzeige des Code Analyzer Reports für die Datei Dateiname.m. (alternativ „Show Code Analyzer Report“ im Editor) Matlab-Profiler Suche nach Optimierungspotential, meist ist nur ein kleiner Teil des Programms für lange Rechenzeiten verantwortlich. Debugging des Matlab-Codes profile on profile viewer Profiling starten. Profile Summary anzeigen. (alternativ: „Run and Time“ im Editor) geschicktes Programmieren in Matlab: Binomialkoeffizient langsame Variante: schnellere Variante: function [ binom ] = binom_l( n,k ) function [ binom ] = binom_s( n,k ) if (n < 0 || k < 0) disp(['Bitte zwei positive,'... 'Zahlen eingeben']); binom = NaN; elseif n >= k if k > n/2, k = n-k; end zaehler = 1; nenner = 1; for l = n-k+1:n zaehler = zaehler * l; end if (n < 0 || k < 0) disp(['Bitte zwei positive,'... 'Zahlen eingeben']); binom = NaN; elseif n >= k if k > n/2, k = n-k; end binom = prod(((n-k+1):n)./(1:k)); else binom = 0; end for l = 1:k nenner = nenner * l; end binom = zaehler / nenner; else binom = 0; end end end Bemerkung: Die schnellere Variante ist zudem stabiler, da für große k die Variablen zaehler und nenner nicht über alle Schranken wachsen. geschicktes Programmieren in Matlab: Funktionsauswertungen langsame Variante: function [x,y] = funkauswert_l( t ) x = zeros(1,length(t)); y = zeros(1,length(t)); for i = 1:length(t) x(i) = t(i)^2; y(i) = log2(t(i)); end end schnelle Variante: function [x,y] = funkauswert_s( t ) x = t.^2; y = log2(t); end geschicktes Programmieren in Matlab: Vandermonde-Matrix 1 1 1 V := . .. 1 x1 x2 x3 .. . xm x12 x22 x32 .. . 2 xm ... ... ... .. . ... x1n−1 x2n−1 x3n−1 .. . n−1 xm häufige Anwendung: Beschreibung einer Polynom-Interpolation. langsame Variante: schnelle Variante: function [ V ] = vander_l( x ) function [ V ] = vander_s( x ) for l = 1:length(x) for m = 1:length(x) V(l,m) = x(l)^(m-1); end end n = x = V = for end end length(x); x(:); %x ist nun Spaltenvektor ones(n); l = 1:n-1 V(:,l+1) = x.*V(:,l); end Bemerkung: Die Matlab-Funktion vander berechnet eine modifizierte Version der Vandermonde-Matrix. geschicktes Programmieren in Matlab Um die Geschwindigkeit der Funktionen zu testen, wird mit dem Matlab-Profiler das folgende Skript analysiert: B1 = ones(100); B2 = ones(100); for n = 1:100 for k = 1:n B1(n,k) = binom_l(n, k); B2(n,k) = binom_s(n, k); end end t = 0:0.00001:1; [f1,f2] = funkauswert_s(t); [g1,g2] = funkauswert_l(t); x = 0.1:0.001:1; v1 = vander_l(x); v2 = vander_s(x); geschicktes Programmieren in Matlab: Ergebnis des Profilers allgemeine Tipps: (Große) Matrizen sollten vor ihrer Verwendung mit der maximal benötigten Größe vorbelegt werden (z.B. mit zeros(M,N)). For-Schleifen sollten (falls möglich) durch Vektorisierung des Codes vermieden werden. Dies führt zu effizienterem und übersichtlicherem Code. Ausgaben im Command Window sowie grafische Ausgaben benötigen viel Zeit und sollten daher nur mit Bedacht eingesetzt werden. Löschen nicht mehr benötigter Variablen (mit clear Name) gibt dadurch belegten Speicher wieder frei. Jeder Aufruf von Skripten oder Funktionen, die in separaten Dateien gespeichert sind, benötigt zusätzlich Zeit. Wird eine Funktion in einer for-Schleife häufig aufgerufen, so erhält man schnelleren Code, indem man den Inhalt der Funktion (entsprechend angepasst) in die Schleife kopiert. Achtung: Dadurch verschlechtert sich die Lesbarkeit sowie die Wartung des Codes, daher nur in geschwindigkeitskritischen Stellen verwenden! Function Handles (@), welche im Wesentlichen mit dem Konzept von Funktionszeigern in C übereinstimmen, können oft gewinnbringend eingesetzt werden. Graphiken mit Matlab Graphiken können direkt in Matlab erstellt werden! figure hold on hold all legend(String1,String2,...) axis equal set(...) / get(...) gcf / gca erzeugt ein neues Figure schützt ein Fenster vor Überschreiben schützt ein Fenster vor Überschreiben, die Linienfarbe wird automatisch gewechselt fügt der Grafik eine Legende hinzu Achseneinheit in alle Richtungen gleich lang Eigenschaften setzen/anzeigen lassen aktuelles Figure- bw. Achsen-Handle 2D-Plots: plot(x,y) plot(y) subplot(m, n, zaehler) plottet den Vektor y gegen den Vektor x plottet den Vektor y gegen dessen Indizes erstellt ein Figure für m × n - Subplots, 1 ≤ zaehler ≤ m · n bezeichnet das aktuelle Fenster, wobei zeilenweise gezählt wird. Allgemeine Syntax: plot(x,y,’FMS’,...,’Eigenschaft’, ’Wert’) 8 7 6 5 FMS = Farbe Marker Linientyp z.B. ’r*:’ zeichnet rote gepunktete Linie mit *-Marker Eigenschaft = ’LineWidth’, ’MarkerSize’,... 4 3 2 1 0 0.2 0.4 0.6 0.8 1 1.2 1.4 1.6 1.8 2 2D-Plot: Beispiel 1 2 3 theta = linspace(-pi,pi,25); sinus = sin(theta); kosinus = cos(theta); 4 5 6 7 8 %figure hold all; plot(theta,sinus); plot(theta,kosinus,'-o','MarkerSize',8); 9 10 11 12 13 14 15 16 17 18 19 20 set(gca,'XTick',-pi:pi/2:pi); set(gca,'XTickLabel',{'-pi','-pi/2','0','pi/2','pi'}); xlabel('-\pi \leq \Theta \leq \pi'); set(gca,'YTick',-1:0.5:1); title('Plot von sin(\Theta) und cos(\Theta)'); text(-pi/4,sin(-pi/4),'\leftarrow sin(-\pi/4)',... 'HorizontalAlignment','left') ; legend('Sinus','Kosinus'); axis([-pi pi -1 1]); axis equal; box; 2D-Plot: Beispiel Plot von sin(Θ) und cos(Θ) Sinus Kosinus 1 0.5 0 0.5 ← sin( π/4) 1 pi pi/2 0 π≤ Θ ≤ π pi/2 pi Der Plot Editor bietet zahlreiche Möglichkeiten der Nachbearbeitung. Bei Speicherung als .fig ist eine Nachbearbeitung auch später noch möglich. Export in alle Standardformate möglich: PNG, JPG, EPS, PDF, ... 3D Grafiken: Beispiele Beispiel: Leastsquare Gegeben: N Messwerte (x1 , y1 ), . . . , (xN , yN ), die als N × 2-Matrix in einer Textdatei (ohne Kommentare) gespeichert sind. Aufgabenstellung: Berechne die zugehörige Bestgerade mit Hilfe der Methode der kleinsten Quadrate (vergleiche Aufgabe 2 auf Übungsblatt 6). Ziele: Schreibe eine Funktion, die, falls nur ein Rückgabewert angefordert wird, nur die Steigung der Geraden berechnet und diese zurückgibt, falls zwei Rückgabewerte angefordert werden, die Steigung und den y -Achsenabschnitt der Geraden zurückgibt, die Punktewolke und die berechnete Gerade plottet, sofern der Parameter Plotschalter den Wert 1 besitzt. Beispiel: Leastsquare 1 2 3 4 5 6 7 8 function [ a,b ] = leastsquare( Dateiname, Plotschalter ) daten = importdata(strcat(Dateiname,'.dat')); [N,M] = size(daten); if M ~= 2 disp('Falsche Dimension!'); end; tquer = sum(daten(:,1))/N; yquer = sum(daten(:,2))/N; %alterantiv: s = sum(daten)/N; tquer2 = s(1); yquer2 = s(2); a = (daten(:,1)-tquer)'*(daten(:,2)-yquer)/sum((daten(:,1)-tquer).^2); 9 10 11 12 13 14 15 16 17 18 19 20 if nargout == 2 || (nargin == 2 && Plotschalter == 1) b = yquer-a*tquer; if nargin == 2 && Plotschalter == 1 figure('Name','leastsquare') hold all plot(daten(:,1),daten(:,2),'x','MarkerSize',10) tmin = min(daten(:,1)); tmax = max(daten(:,1)); plot([tmin;tmax],[a*tmin+b;a*tmax+b],'r','LineWidth',2); end end 21 22 end Beispiel: Leastsquare Datensatz: leastsquare.dat (siehe Homepage, Übung) leastsquare(’leastsquare’,1); weiterer Datensatz leastsquare(’testdaten’,1); Erstellen von Filmen mit Matlab F = getframe(H) movie(F,n) Obj = VideoWriter(Dateiname); open(Obj); writeVideo(Obj,F) close(Obj); Kreieren eine Bildabfolge, Figure H wird in eine spezielle Struktur F (Frame) gespeichert. n-maliges Abspielen des Frames F Erstellung des Objekts Obj, um Videos in eine .avi-Datei zu schreiben Öffnen der Datei, welche Obj zugewiesen ist. Schreibt den Frame in die Obj zugewiesenen Datei. Schließen der Datei, welche Obj zugewiesen ist. Beispiel: (primitive) Simulation eines Aktienkurses Teil 1 des Skriptes: Parameter setzen und Werte berechnen. 1 2 3 4 5 6 %% Parameter startwert = 100; tage = 30; anzahl = 20; max_schwank = 0.2*startwert; tendenz = 0.01; 7 8 9 10 11 12 13 14 %% Berechnung wert = zeros(anzahl,tage+1); wert(:,1) = startwert; for k = 1:tage wert(:,k+1) = wert(:,k)+2*(rand(anzahl,1)-0.5+tendenz)*max_schwank; end disp(['geschätzter Endwert: ', num2str(mean(wert(:,tage+1)),'%.2f')]); Beispiel: (primitive) Simulation eines Aktienkurses Teil 2 des Skriptes: Movie erzeugen und als aktie.avi speichern. 1 2 3 4 5 6 %% Figure und Movie figure; hold on; axis([1 tage startwert-5*max_schwank startwert+10*max_schwank]) t = linspace(1,tage+1,tage+1); col = lines(anzahl); 7 8 9 10 11 12 13 14 15 16 mov(anzahl*(tage-1)) = struct('cdata',[],'colormap',[]); hilf = 0; for l = 1:anzahl for k = 2:tage plot(t(k-1:k),wert(l,k-1:k),'LineWidth',2,'color',col(l,:)); hilf = hilf + 1; mov(hilf) = getframe(gcf); end end 17 18 19 20 21 writerObj = VideoWriter('aktie.avi'); open(writerObj); writeVideo(writerObj,mov); close(writerObj); Beispiel: (primitive) Simulation eines Aktienkurses Simulation von 30 Aktienkursen 300 250 Wert der Aktie 200 150 100 50 0 5 10 15 Tag 20 25 30