Einführung in das Programmieren für Technische Mathematik¶

  • Es ist egal welche Sprache man lernt
  • Kann man eine Programmiersprache, dann kann man alle!
  • C & C++¶

    • Perfekt für Anwendungen in denen Performance gefragt ist
    • Lehrt die inneren Vorgänge im Computer
    • Erlaubt Abstraktion

    Programmieren für Mathematiker¶

    • Das ist keine Programmierkurs der Informatik
    • Wenig Softwaredesign
    • Wenig Debugging
    • Wenige Algorithmen

    Wichtige Infos zu EPROG¶

    • Info-Post im TUWEL-Forum

      • Lernziele
      • Regeln & Pflichten
      • Benotungsschema
      • Evaluation & Notenspiegel
    • TUWEL

      • Download der Folien & Übungsblätter
      • freiwilliges UE-Material (alte Tests!)
      • Termine
      • Zoom-Links

    Literatur¶

    • VO-Folien aus letzen Semestern auf TUWEL
    • aktuelle Folien wöchentlich auf TUWEL
    • formal keine weitere Literatur nötig

    • mehrere Bücher zum Download auf TUWEL, z.B.

      • Ralf Kirsch, Uwe Schmitt Programmieren in C, eine mathematikorientierte Einführung
      • Klaus Schmaranz Softwareentwicklung in C / C++
      • Online: google.com, stackoverflow.com

    Programm¶

    • Wikipedia: Ein Computerprogramm oder kurz Programm ist eine Folge von Anweisungen, die den Regeln einer Programmiersprache genügen, um auf einem Computer eine bestimmte Funktionalität, Aufgaben- oder Problemstellung bearbeiten oder lösen zu können.

    • Anweisungen = Deklarationen und Instruktionen

      • Deklaration = z.B. Definition von Variablen
      • Instruktion = "tue etwas"
    • BSP: Sei x∈Rx∈R

    • BSP: Definiere f:R→Rf:R→R, f(x)=x3f(x)=x3
    • BSP: suche einen Telefonbucheintrag
    • BSP: berechne den Wert eines Integrals

    Algorithmus¶

    • Wikipedia: Ein Algorithmus ist eine aus endlich vielen Schritten bestehende, eindeutige und ausführbare Handlungsvorschrift zur Lösung eines Problems oder einer Klasse von Problemen.

    • BSP: Berechne die Lösung eines linearen Gleichungssystems mittels Gauß-Elimination

    • BSP: Berechne die Nullstelle eines quadratischen Polynoms mittels pp-qq-Formel

    • IdR. unendlich viele Algorithmen für ein Problem

    • IdR. sind Algorithmen unterschiedlich "gut"

      • Was "gut" genau heißt erfahren wir später

    Programmiersprachen¶

    • Grobe Unterscheidung in Interpreter- und Compiler-basierte Sprachen
    • Interpreter führt Source-Code zeilenweise bei der Übersetzung aus
      • d.h. Übersetzen & Ausführen ist gleichzeitig
      • z.B. Matlab, Java, PHP, Python
    • Compiler übersetzt Source-Code in ein ausführbares Programm (Executable)
      • Executable ist eigenständiges Programm
      • d.h. (1) Übersetzen, dann (2) Ausführen
      • z.B. C, C++, Fortran
    • bessere Unterscheidung (siehe Schmaranz)
      • imperative Sprachen, z.B. Matlab, C, Fortran
      • objektorientierte, z.B. C++, Java, Python
      • funktionale, z.B. Lisp

    Achtung¶

    • C ist Compiler-basierte Programmiersprache
    • Compilierter Code ist systemabhängig,

      • d.h. Code läuft idR. nur auf dem System, auf dem er compiliert wurde
    • Source-Code ist systemunabhängig,

      • d.h. er kann auch auf anderen Systemen compiliert werden.
    • C-Compiler unterscheiden sich leicht
      • C ist eine sehr alte Sprache (erschienen 1972)
      • vielfach weiterentwickelt
      • in der VO: ANSI-C = C89 (Standard von 1989)

    Wie erstellt man ein C-Programm?¶

    • Öffne eine (ggf. neue) Datei name.c in einem Editor
      • Endung .c ist Kennung eines C-Programms
    • Schreibe den sog. Source-Code (= C-Programm)
    • Source-Code abspeichern
    • Compilieren z.B. mit Eingabe gcc name.c in der Shell
    • Falls Code fehlerfrei, erhält man Executable a.out unter Windows: a.exe
    • Diese wird durch a.out bzw. ./a.out gestartet
    • Compilieren mit gcc name.c -o output erzeugt Executable output statt a.out

    Das erste C-Programm¶

    • Zeilennummern und Farben gehören nicht zum Code (sind lediglich Orientierungshilfe auf den Folien)
    • Jedes C-Programm besitzt die Zeilen 3 und 6.
    • Die Ausführung eines C-Programms startet immer bei main() - egal, wo main() im Code steht
    • Klammern {...} schließen in C sog. Blöcke ein
    In [3]:
    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    
    int main(){
        printf("Hello World\n");
        return 0;
    }
    
    Hello World
    

    Das erste C-Programm¶

    • Hauptprogramm main() bildet immer einen Block
    • Logische Programmzeilen enden mit Semikolon, vgl. Zeile 4 & 5
    • printf gibt Text aus (in Anführungszeichen),
      • \n macht einen Zeilenumbruch
    • Anführungszeichen müssen in derselben Zeile sein
    • Zeile 1: Einbinden der Standardbibliothek für Input-Output (später mehr!)
    In [16]:
    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    
    int main(){     
        printf("Hello World!\n"); 
        return 0;
    }
    
    Hello World!
    

    Das erste C-Programm¶

    Achtung¶

    • Viele Compiler erlauben das Weglassen von int und return 0;
    • Gut zu wissen falls Sie fremden Code verwenden
    In [19]:
    1
    2
    3
    4
    5
    #include <stdio.h>
    
    main(){
        printf("Hello World!\n");
    }
    
    /tmp/tmpexq2n71l.c: In function ‘main’:
    /tmp/tmpexq2n71l.c:6:7: error: expected ‘;’ before ‘{’ token
        6 | main(){
          |       ^
          |       ;
    [C kernel] GCC exited with code 1, the executable will not be executed

    Fehler¶

    Syntaxfehler¶

    • Syntax = Wortschatz (Befehle) & Grammatik einer Sprache (Was man wie verbinden kann...)
    • Syntaxfehler = Falsche Befehle oder Verwendung
      • Compiler gibt Fehlermeldung

    Laufzeitfehler¶

    • Fehler, der erst bei Programm-Ausführung auftritt
      • erst bei komplexeren Programmen möglich
      • viel schwerer zu finden
      • durch sorgfältiges Arbeiten möglichst vermeiden

    Beispiel für Syntaxfehler¶

    Der Kompiler kann die Funktion printf nicht finden, da die Bibiliothek stdio.h nicht eingebunden wurde.

    In [22]:
    1
    2
    3
    4
    int main(){
        printf("Hello World!\n");
        return 0;
    }
    
    /tmp/tmpzwdt271q.c: In function ‘main’:
    /tmp/tmpzwdt271q.c:2:5: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
        2 |     printf("Hello World!\n");
          |     ^~~~~~
    /tmp/tmpzwdt271q.c:2:5: warning: incompatible implicit declaration of built-in function ‘printf’
    /tmp/tmpzwdt271q.c:1:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
      +++ |+#include <stdio.h>
        1 | int main(){
    
    Hello World!
    

    Beispiel für Syntaxfehler¶

    Der Kompiler beschwert sich über das fehlende Semikolon ; in Zeile 4.

    In [27]:
    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    
    int main(){
        printf("Hello World!\n")
        return 0;
    }
    
    Hello World!
    

    Die ersten Sprachelemente¶

    Variablen¶

    • Variable = symbolischer Name für Speicherbereich
    • Variable in Math. und Informatik verschieden:
      • Mathematik: Sei x∈Rx∈R fixiert xx
      • Informatik: x = 5 weist x den Wert 55 zu, Zuweisung kann jederzeit geändert werden z.B. x = 7

    Variablen-Namen¶

    • bestehen aus Zeichen, Ziffern und Underscore _

      • maximale Länge = 31
      • erstes Zeichen darf keine Ziffer sein
    • Klein- und Großschreibung wird unterschieden

      • d.h. Var, var, VAR sind 3 verschiedene Variablen
    • Konvention: Namen sind klein_mit_underscores

    Datentypen¶

    • Bevor man Variable benutzen darf, muss man idR. erklären, welchen Typ Variable haben soll

    • Elementare Datentypen:

      • Gleitkommazahlen (ersetzt QQ, RR), z.B. double
      • Integer, Ganzzahlen (ersetzt NN, ZZ), z.B. int
      • Zeichen (Buchstaben) char
    • int x; deklariert Variable x vom Typ int

    Deklaration¶

    • Deklaration = das Anlegen einer Variable
      • d.h. Zuweisung von Speicherbereich auf einen symbolischen Namen & Angabe des Datentyps
        • Zeile int x; deklariert Variable x vom Typ int
        • Zeile double var; deklariert var vom Typ double
    In [ ]:
    1
    2
        int x;
        double var;
    

    Initialisierung¶

    • Durch Deklaration einer Variablen wird lediglich Speicherbereich zugewiesen
    • Falls noch kein konkreter Wert zugewiesen:
      • Wert einer Variable ist zufällig
    • Deshalb direkt nach Deklaration der neuen Variable Wert zuweisen, sog. Initialisierung
      • int x; (Deklaration)
      • x = 0; (Initialisierung)
    • Deklaration & Initialisierung auch in einer Zeile möglich: int x = 0;
    In [ ]:
    1
    2
    int x = 0;
    double var=3.1415;
    
    • Einbinden der Input-Output-Funktionen (Zeile 1)

      • printf gibt Text (oder Wert einer Var.) aus
      • scanf liest Tastatureingabe ein in eine Variable
    • Prozentzeichen % in Zeile 7/8 leitet Platzhalter ein

    In [31]:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    
    int main() {
      int x = 0;
    
      printf("Input: x=");
      scanf("%d",&x);
      printf("Output: x=%d\n",x);
    }
    
    Input: x=4
    Output: x=4, 12
    

    Es gibt verschiedene Platzhalter für verschiedene Datentypen.

    Datentyp Platzhalter printf Platzhalter scanf
    int %d %d
    double %f %lf
    char %c %c
    In [32]:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    
    int main() {
      double x = 0;
    
      printf("Input: x=");
      scanf("%lf",&x);
      printf("Output: x=%f\n",x);
    }
    
    Input: x=3.1234
    Output: x=3.123400
    
    • Beachte & bei scanf in Zeile 7

      • scanf("%d", & x)
      • aber: printf("%d", x)
    • Wenn man & vergisst ⇒⇒ Laufzeitfehler

      • Compiler merkt Fehler nicht (kein Syntaxfehler!)
      • Sorgfältig arbeiten!
    In [34]:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    
    int main() {
      int x = 0;
    
      printf("Input: x=");
      scanf("%d",x);
      printf("Output: x=%d\n",x);
    }
    
    Input: x=3
    
    [C kernel] Executable exited with code -11

    Achtung bei double nicht %lf vergessen!

    In [36]:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    
    int main() {
      double x = 0;
    
      printf("Input: x=");
      scanf("%f",&x);
      printf("Output: x=%f\n",x);
    }
    
    Input: x=3.1415
    Output: x=3.141500
    

    Zuweisungsoperator =¶

    • Das einfache Gleich = ist Zuweisungsoperator

      • Zuweisung immer rechts nach links!
    • Zeile x = 1; weist den Wert auf der rechten Seite der Variablen x zu

    • Zeile x = y; weist den Wert der Variablen y der Variablen x zu

      • insb. haben x und y danach denselben Wert
      • d.h. Vertauschen der Werte nur mit Hilfsvariable
    In [37]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>
    
    int main() {
      int x = 1;
      int y = 2;
    
      int tmp = 0;
    
      printf("a) x=%d, y=%d, tmp=%d\n",x,y,tmp);
      
      tmp = x;
      x = y;
      y = tmp;
    
      printf("b) x=%d, y=%d, tmp=%d\n",x,y,tmp);
    }
    
    a) x=1, y=2, tmp=0
    b) x=2, y=1, tmp=1
    

    Arithmetische Operatoren¶

    • Bedeutung eines Operators kann vom Datentyp abhängen!

    • Operatoren auf Ganzzahlen:

      • a=b, -a (Vorzeichen)
      • a+b, a-b, a*b, a/b (Division ohne Rest),
        • a%b (Divisionsrest)
        • a+=b etc. gleichbedeutend mit a=a+b
    • Operatoren auf Gleitkommazahlen:

      • a=b, -a (Vorzeichen)
      • a+b, a-b, a*b, a/b ("normale" Division)
      • a+=b etc. gleichbedeutend mit a=a+b

    Arithmetische Operatoren¶

    • Achtung: 2/3 ist Ganzzahl-Division, also Null!

    • Notation für Gleitkommazahlen:

      • Vorzeichen -, falls negativ
      • Vorkommastellen
      • Dezimalpunkt
      • Nachkommastellen
      • e oder E mit ganzzahligem Exponenten (10er Potenz!), z.B. 2e2=2E2=2⋅102=2002e2=2E2=2⋅102=200
      • Wegfallen darf entweder Vor- oder Nach- kommastelle (sonst sinnlos, da kein Wert!)
      • Wegfallen darf entweder Dezimalpunkt oder e bzw. E mit Exponent (sonst wird die Zahl als Integer verstanden, vgl. 2/3)
    • Also: 2./3. ist Gleitkommadivision ≈0.6666666666666667≈0.6666666666666667 (bei double ca. auf 16 Stellen genau)

    Type Casting¶

    • Operatoren können auch Variablen verschiedener Datentypen verbinden
    • Vor der Ausführung werden beide Variablen auf denselben Datentyp gebracht (Type Casting)

    • Welchen Datentyp hat x+y in Zeile 7, 8?

      • Den mächtigeren Datentyp, also double!
      • so wie Z⊂RZ⊂R, so ist double mächtiger als int
      • Type Casting von Wert x auf double
    • Zeile 7: Type Casting, da double auf int Zuweisung
      • durch Abschneiden, nicht durch Rundung!
    In [5]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    #include <stdio.h>
    
    int main() {
      int x = 1;
      double y = 2.8;
    
      int sum_int = x+y;
      double sum_dbl = x+y;
    
      printf("sum_int = %d\n",sum_int);
      printf("sum_dbl = %f\n",sum_dbl);
    }
    
    sum_int = 3
    sum_dbl = 3.800000
    

    Implizites Typecasting¶

    • Warum Ergebnis 00 in a) und d) ?

      • 2, 3 sind int ⇒⇒ 2/3 ist Ganzzahl-Division
        • Werden Variablen verschiedenen Typs durch arith. Operator verbunden, Type Casting auf "gemeinsamen" (mächtigeren) Datentyp
        • vgl. Zeile 5, 13, 14
        • 2 ist int, 3. ist double ⇒⇒ 2/3. ergibt double
      • Der C-Compiler setzt die kanonischen Regeln um:
        • Punkt-Rechnung vor Strich-Rechnung
        • gleichwertige Operatoren von links nach rechts
        • vgl. Zeile 13, 14
    In [6]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    
    int main() {
      double dbl1 = 2 / 3;
      double dbl2 = 2 / 3.;
      double dbl3 = 1E2;
      int int1 = 2;
      int int2 = 3;
     
      printf("a) %f\n",dbl1);
      printf("b) %f\n",dbl2);
    
      printf("c) %f\n",dbl3 * int1 / int2);
      printf("d) %f\n",dbl3 * (int1 / int2) );
    }
    
    a) 0.000000
    b) 0.666667
    c) 66.666667
    d) 0.000000
    

    Explizites Type Casting¶

    • Kann dem Compiler mitteilen, in welcher Form eine Variable interpretiert werden muss
      • Dazu Ziel-Typ in Klammern voranstellen!
    • In Zeile 7, 8, 9: Explizites Type Casting (jeweils von int zu double)
    • In Zeile 6, 8, 9: Implizites Type Casting
    In [7]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    
    int main() {
      int a = 2;
      int b = 3;
      double dbl1 = a / b;
      double dbl2 = (double) (a / b);
      double dbl3 = (double) a / b;
      double dbl4 = a / (double) b;
    
      printf("a) %f\n",dbl1);
      printf("b) %f\n",dbl2);
      printf("c) %f\n",dbl3);
      printf("d) %f\n",dbl4);
    }
    
    a) 0.000000
    b) 0.000000
    c) 0.666667
    d) 0.666667
    

    Fehlerquelle beim Type Casting¶

    • Implizites Type Casting sollte man vermeiden!
      • d.h. Explizites Type Casting verwenden!
    • Bei Rechnungen Zwischenergebnisse in richtigen Typen speichern!
    In [8]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #include <stdio.h>
    
    int main() {
      int a = 2;
      int b = 3;
      double dbl = (double) a / b;
    
      int i = dbl;
    
      printf("a) %f\n",dbl);
      printf("b) %f\n",dbl*b);
      printf("c) %d\n",i);
      printf("d) %d\n",i*b);
    }
    
    a) 0.666667
    b) 2.000000
    c) 0
    d) 0
    

    Einfache Verzweigung¶

    Logische Operatoren¶

    • Es seien a,b zwei Variablen (auch versch. Typs!)
      • Vergleich (z.B. a < b) liefert Wert 1, falls wahr, bzw. 0, falls falsch
    • Übersicht über Vergleichsoperatoren:

      Symbol Bedeutung
      == Gleichheit (ACHTUNG mit Zuweisung!)
      != Ungleichheit
      > echt größer
      < echt kleiner
      >= größer oder gleich
      <= kleiner oder gleich
    • Bei Vergleichen stets Klammern setzen (häufig unnötig aber oft besser lesbar)

    • Weitere logische Operatoren

    Symbol Bedeutung
    ! Nicht
    && und
    || oder

    Logische Verkettung¶

    • Warum ist Aussage in Zeile 10 falsch, aber in Zeile 13 wahr?
      • Auswertung von links nach rechts:
      • a > b ist wahr, also mit 1 bewertet
      • 1 > c ist falsch, also mit 0 bewertet
      • Also wird a > b > c mit falsch bewertet!
      • Aussage in 10 ist also nicht korrekt formuliert!
    In [9]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    
    int main() {
      int result = 0;
    
      int a = 3;
      int b = 2;
      int c = 1;
      
      result = (a > b > c);
      printf("a) result=%d\n",result);
    
      result = (a > b) && (b > c);
      printf("b) result=%d\n",result);
    }
    
    /tmp/tmpl7rt034j.c: In function ‘main’:
    /tmp/tmpl7rt034j.c:10:15: warning: comparisons like ‘X<=Y<=Z’ do not have their mathematical meaning [-Wparentheses]
       10 |   result = (a > b > c);
          |             ~~^~~
    
    a) result=0
    b) result=1
    

    if-else¶

    • einfache Verzweigung: Wenn - Dann - Sonst
    • if ( condition) statementA else statementB
    • nach if steht Bedingung stets in runden Klammern
    • nach Bedingung steht nie Semikolon
    • Bedingung ist falsch, falls sie 00 ist bzw. mit 00 bewertet wird, sonst ist die Bedingung wahr
      • Bedingung wahr ⇒⇒ statementA wird ausgeführt
      • Bedingung falsch ⇒⇒ statementB wird ausgeführt
    • Statement ist
      • entweder eine Zeile (d.h. bis zum Semikolon!)
      • oder mehrere Zeilen in geschwungenen Klammern { ... }, sog. Block
    • else-Zweig ist optional
      • d.h. else statementB darf entfallen

    Beispiel zu if¶

    • abhängige Zeilen einrücken (Lesbarkeit!)
    • WARNUNG: Nicht-Verwendung von Blöcken {...} ist fehleranfällig (Zeile 9--10)
      • besser: Block (analog zu Zeile 12--14)
    • könnte zusätzlich else in Zeile 11 schreiben
      • da if's sich ausschließen
    In [10]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    
    int main() {
      int x = 0;
    
      printf("Input x=");
      scanf("%d",&x);
    
      if (x < 0)
        printf("x=%d is negative\n",x);
      
      if (x > 0) {
        printf("x=%d is positive\n",x);
      }
    }
    
    Input x=3
    x=3 is positive
    

    Beispiel zu if-else¶

    • Eine Bedingung ist wahr, falls Wert ≠0≠0
      • z.B. Zeile 15, aber besser: if ( var2!= 0)
    In [11]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <stdio.h>
    
    int main() {
      int var1 = -5;
      double var2 = 1e-32;
      int var3 = 5;
    
      if (var1 >= 0) {
        printf("var1 >= 0\n");
      }
      else {
        printf("var1 < 0\n");
      }
    
      if (var2) {
        printf("var2 != 0, i.e., cond. is true\n");
      }
      else {
        printf("var2 == 0, i.e., cond. is false\n");
      }
      
      if ( (var1 < var2) && (var2 < var3) ) {
        printf("var2 lies between the others\n");
      }
    }
    
    var1 < 0
    var2 != 0, i.e., cond. is true
    var2 lies between the others
    

    Gerade oder Ungerade?¶

    • Programm überprüft, ob eingegebene Zahl x gerade Zahl ist oder nicht
    • Man kann Verzweigungen schachteln:
      • Einrückungen machen Code übersichtlicher
      • formal nicht notwendig, aber trotzdem!
      • Abhängigkeiten werden verdeutlicht
    In [12]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>
    
    int main() {
      int x = 0;
    
      printf("Input x=");
      scanf("%d",&x);
    
      if (x > 0) {
        if (x%2 != 0) {
          printf("x=%d is odd\n",x);
        }
        else {
          printf("x=%d is even\n",x);
        }
      }
      else {
        printf("Error: Input has to be positive!\n");
      }
    }
    
    Input x=3
    x=3 is odd
    

    Zwei Zahlen aufsteigend sortieren¶

    • Eingabe von zwei Zahlen x1,x2∈Rx1,x2∈R Zahlen werden aufsteigend sortiert
      • ggf. vertauscht
    • Ergebnis wird ausgegeben
    In [13]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>
    
    int main() {
      double x1 = 0;
      double x2 = 0;
      double tmp = 0;
    
      printf("Unsorted input:\n");
      printf(" x1=");
      scanf("%lf",&x1);
      printf(" x2=");
      scanf("%lf",&x2);
      
      if (x1 > x2) {
        tmp = x1;
        x1 = x2;
        x2 = tmp;
      }
      
      printf("Sorted output (ascending order):\n");
      printf(" x1=%f\n",x1);
      printf(" x2=%f\n",x2);
    }
    
    Unsorted input:
     x1=3
     x2=1
    Sorted output (ascending order):
     x1=1.000000
     x2=3.000000
    

    Innen oder Außen?¶

    • zz liegt im Kreis um xx mit Radius rr, falls ∥x−z∥<r‖x−z‖<r
    • zz auf Kreisrand, falls ∥x−z∥=r‖x−z‖=r
    • ∥x−z∥2=(x1−z1)2+(x2−z2)2‖x−z‖2=(x1−z1)2+(x2−z2)2
    In [14]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #include <stdio.h>
    
    int main() {
      double r = 0;
      double x1 = 0;
      double x2 = 0;
      double z1 = 0;
      double z2 = 0;
      double dist2 = 0;
    
      printf("Radius of the circle: r=");
      scanf("%lf",&r);
      printf("Center of the circle: x = (x1,x2)\n");
      printf(" x1=");
      scanf("%lf",&x1);
      printf(" x2=");
      scanf("%lf",&x2);
      printf("Point in the plane: z = (z1,z2)\n");
      printf(" z1=");
      scanf("%lf",&z1);
      printf(" z2=");
      scanf("%lf",&z2);
    
      dist2 = (x1-z1)*(x1-z1) + (x2-z2)*(x2-z2);
      if ( dist2 < r*r ) {
          printf("z is inside the circle\n");
      }
      else { 
        if ( dist2 > r*r ) {
          printf("z is outside of the circle\n");
        }
        else {
          printf("z lies on the boundary of the circle\n");
        }
      }
    }
    
    Radius of the circle: r=3
    Center of the circle: x = (x1,x2)
     x1=0
     x2=0
    Point in the plane: z = (z1,z2)
     z1=1
     z2=2
    z is inside the circle
    

    Gleichheit vs. Zuweisung¶

    • Nur Erinnerung: if (a==b) vs. if (a=b)
      • beides ist syntaktisch korrekt!
    • if (a==b) ist Abfrage auf Gleichheit
      • ist vermutlich so gewollt...
    • ABER: if (a=b)
      • weist a den Wert von b zu
      • dann Abfrage, ob a≠0a≠0
    • Letzteres ist schlechter Programmierstil!
      • da zwei Aktionen in einer Programmzeile

    Strukturierung mit Blöcken¶

    Lifetime & Scope¶

    • Lifetime einer Variable = Zeitraum, in dem Speicherplatz zugewiesen ist = Zeitraum, in dem Variable existiert

    • Scope einer Variable = Zeitraum, in dem Variable sichtbar ist = Zeitraum, in dem Variable gelesen/verändert werden kann

    • Scope ⊆⊆ Lifetime

    Globale & Lokale Variablen¶

    • globale Variablen = Variablen, die globale Lifetime haben (bis Programm terminiert)

      • eventuell lokaler Scope
      • werden am Anfang außerhalb von main deklariert
    • lokale Variablen = Variablen, die nur lokale Lifetime haben

    • Konvention: erkenne Variable am Namen
      • lokale Variablen sind klein_mit_underscores
      • globale Var. haben auch_underscore_hinten_

    Blöcke¶

    • Blöcke stehen innerhalb von {...}

    • Jeder Block startet mit Deklaration zusätzlich benötigter Variablen

      • Variablen sollten nur am Anfang eines Blocks deklariert werden
    • Die innerhalb des Blocks deklarierten Variablen werden nach Blockende vergessen (= gelöscht)

      • d.h. Lifetime endet
      • lokale Variablen
    • Schachtelung {..{..}..}

      • beliebige Schachtelung ist möglich
      • Variablen aus äußerem Block können im inneren Block gelesen und verändert werden, umgekehrt nicht. Änderungen bleiben wirksam.
      • d.h. Lifetime & Scope nur nach Innen vererbt
      • Wird im äußeren und im inneren Block Variable var deklariert, so wird das "äußere" var überdeckt und ist erst wieder ansprechbar (mit gleichem Wert wie vorher), wenn der innere Block beendet wird.
        • d.h. äußeres var ist nicht im inneren Scope
        • Das ist schlechter Programmierstil!
        • siehe die Beispiele auf den nächsten 2 Folien!

    Einfaches Beispiel¶

    • zwei verschiedene lokale Variablen x
      • Deklaration + Initialisierung (Zeile 4, 9)
      • unterscheide von Zuweisung (Zeile 6)
    In [15]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    #include <stdio.h>
    
    int main() {
      int x = 7;
      printf("a) %d\n", x);
      x = 9;
      printf("b) %d\n", x);
      {
        int x = 17;
        printf("c) %d\n", x);
      }
      printf("d) %d\n", x);
    }
    
    a) 7
    b) 9
    c) 17
    d) 9
    

    Komplizierteres Beispiel¶

    • zwei Variablen mit Name var0 (Zeile 3 + 18)
      • Namenskonvention absichtlich verletzt
    • zwei Variablen mit Name var1 (Zeile 6 + 11)
    In [16]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>
    
    int var0 = 5;
    
    int main() {
      int var1 = 7;
      int var2 = 9;
    
      printf("a) %d, %d, %d\n", var0, var1, var2);
      {
        int var1 = 17;
    
        printf("b) %d, %d, %d\n", var0, var1, var2);
        var0 = 15;
        var2 = 19;
        printf("c) %d, %d, %d\n", var0, var1, var2);
        {
          int var0 = 25;
          printf("d) %d, %d, %d\n", var0, var1, var2);
        }
      }
      printf("e) %d, %d, %d\n", var0, var1, var2);
    }
    
    a) 5, 7, 9
    b) 5, 17, 9
    c) 15, 17, 19
    d) 25, 17, 19
    e) 15, 7, 19
    

    Funktionen¶

    Beispiel: Quadrieren¶

    • Compiler muss Funktion vor Aufruf kennen

      • d.h. Funktion vor aufrufender Zeile definieren
    • Ausführung startet immer bei main()

    • Die Variable x in Funktion square() und die Variable x in Funktion main() sind verschieden!

    In [17]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    #include <stdio.h>
    
    double square(double x) {
      return x*x;
    }
    
    int main() {
      double x = 0;
      printf("Input x = ");
      scanf("%lf",&x);
      printf("%f^2 = %f\n",x,square(x));
    }
    
    Input x = 3
    3.000000^2 = 9.000000
    

    Funktionen¶

    • Funktion = Zusammenfassung mehrerer Anweisungen zu einem aufrufbaren Ganzen

      • output = function(input)
      • Eingabeparameter input
      • Ausgabeparameter (Return Value) output
    • Warum Funktionen?

      • Zerlegung eines großen Problems in überschaubare kleine Teilprobleme
      • Strukturierung von Programmen (Abstraktionsebenen)
      • Wiederverwertung von Programm-Code
    • Funktion besteht aus Signatur und Rumpf (Body)

      • Signatur = Funktionsname & Eingabe-Ausgabeparameter
      • Anzahl & Reihenfolge ist wichtig!
      • Rumpf = Programmzeilen der Funktion

    Namenskonvention¶

    • lokale Variablen sind klein_mit_underscores
    • globale Var. haben auch_underscore_hinten_
    • Funktionen sind erstesWortKleinKeineUnderscores

    Funktionen in C¶

    • In C können Funktionen

      • mehrere (oder keinen) Parameter übernehmen
      • einen einzigen oder keinen Rückgabewert liefern
      • Rückgabewert muss elementarer Datentyp sein
      • z.B. double, int
    • Signatur hat folgenden Aufbau <type of return value> <function name>(parameters)

      • Funktion ohne Rückgabewert:
      • <type of return value> = void
      • Sonst: <type of return value> = Variablentyp
      • parameters = Liste der Übergabeparameter
      • getrennt durch Kommata
      • vor jedem Parameter Variablentyp angeben
      • kein Parameter ⇒⇒ leere Klammer ()

    Funktionen in C¶

    • Rumpf ist ein Block
      • Rücksprung ins Hauptprogramm mit return oder bei Erreichen des Funktionsblock-Endes, falls Funktionstyp = void
      • Rücksprung ins Hauptprogramm mit
    • return output, falls die Variable output zurückgegeben werden soll
      • Häufiger Fehler: return vergessen
      • Dann Rückgabewert zufällig!
      • ⇒⇒ Irgendwann Chaos (Laufzeitfehler!)

    Variablen¶

    • Alle Variablen, die im Funktionsblock deklariert werden, sind lokale Variablen

    • Alle elementaren Variablen, die in Signatur deklariert werden, sind lokale Variablen

    • Funktion bekommt Input-Parameter als Werte, ggf. Type Casting!

    Call by Value¶

    • Dass bei Funktionsaufrufen Input-Parameter in lokale Variablen kopiert werden, bezeichnet man als Call by Value
      • Es wird neuer Speicher angelegt, der Wert der Eingabe-Parameter wird in diese kopiert

    Beispiel: Minimum zweier Zahlen¶

    • Programm erfüllt Aufgabenstellung der UE:
      • Funktion mit gewisser Funktionalität
      • aufrufendes Hauptprogramm mit
      • Daten einlesen
      • Funktion aufrufen
      • Ergebnis ausgeben
    In [18]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <stdio.h>
    
    double min(double x, double y) {
      if (x > y) {
        return y;
      }
      else {
        return x;
      }
    }
    
    int main() {
      double x = 0;
      double y = 0;
      
      printf("Input x = ");
      scanf("%lf",&x);
      printf("Input y = ");
      scanf("%lf",&y);
      printf("min(x,y) = %f\n",min(x,y));
    }
    
    Input x = 3
    Input y = 4
    min(x,y) = 3.000000
    

    Deklaration von Funktionen¶

    • Bei vielen Funktionen wird Code unübersichtlich

      • Alle Funktionen oben deklarieren, vgl. Zeile 3
      • Compiler weiß dann, wie Funktion agiert
      • vollständiger Fkt.code folgt, vgl. Zeile 16-23
    • Alternative Deklaration = Fkt.code ohne Rumpf

      • double min(double x, double y); vgl. Zeile 3, 16
    • in Literatur: Forward Declaration und Prototyp

    In [19]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>
    
    double min(double, double);
    
    int main() {
      double x = 0;
      double y = 0;
      
      printf("Input x = ");
      scanf("%lf",&x);
      printf("Input y = ");
      scanf("%lf",&y);
      printf("min(x,y) = %f\n",min(x,y));
    }
    
    double min(double x, double y) {
      if (x > y) {
        return y;
      }
      else {
        return x;
      }
    }
    
    Input x = 3
    Input y = 4
    min(x,y) = 3.000000
    

    Beispiel zu Call by Value¶

    • Die Funktionsparameter werden auf lokale Variablen kopiert
    • Später werden wir auch "Call by Reference" kennenlernen
      • da wird tatsächlich die Variable selbst (oder eine Referenz auf die Variable) übergeben
    In [20]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #include <stdio.h>
    
    void test(int x) {
      printf("a) x=%d\n", x);
      x = 43;
      printf("b) x=%d\n", x);
    }
    
    int main() {
      int x = 12;
      printf("c) x=%d\n", x);
      test(x);
      printf("d) x=%d\n", x);
    }
    
    c) x=12
    a) x=12
    b) x=43
    d) x=12
    

    Type Casting & Call by Value¶

    • Type Casting von int auf double bei Übergabe
    In [21]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    
    double divide(double, double);
    
    int main() {
      int int1 = 2;
      int int2 = 3;
     
      printf("a) %f\n", int1 / int2 );
      printf("b) %f\n", divide(int1,int2));
    }
    
    double divide(double dbl1, double dbl2) {
      return(dbl1 / dbl2);
    }
    
    a) 0.000000
    b) 0.666667
    

    Type Casting (Negativbeispiel!)¶

    • Eigentlich x≠yx≠y!
      • Implizites Type Casting von double auf int durch Abschneiden, denn Input-Parameter sind int
    • Achtung mit Type Casting bei Funktionen!
    In [22]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include <stdio.h>
    
    int isEqual(int, int);
    
    int main() {
      double x = 4.1;
      double y = 4.9;
      
      if (isEqual(x,y)) {
          printf("x == y\n");
      }
      else {
          printf("x != y\n");
      }
    }
    
    int isEqual(int x, int y) {
      if (x == y) {
        return 1;
      }
      else {
        return 0;
      }
    }
    
    x == y
    

    Kommentare im Code¶

    Kommentarzeilen¶

    • werden vom Interpreter/Compiler ausgelassen
    • nur für den Leser des Programmcodes
    • notwendig, um eigene Programme auch später noch zu begreifen
      • deshalb brauchbar für Übung!
    • notwendig, damit andere den Code verstehen
      • soziale Komponente der Übung?
    • extrem brauchbar zum debuggen

      • Teile des Source-Code "auskommentieren", sehen was passiert...
      • vor allem bei Fehlermeldungen des Parser
    • Wichtige Regeln:

      • nie dt. Sonderzeichen verwenden
      • nicht zu viel und nicht zu wenig
      • jetzt am Anfang mehr, später weniger!
      • bei großen Projekten mit mehreren AutorInnen Name & letzte Änderung kommentieren
      • vermeidet das Arbeiten an alten Versionen...

    Kommentarzeilen in C¶

    • Gibt in C zwei Typen von Kommentaren:

      • einzeiliger Kommentar
        • eingeleitet durch //, geht bis Zeilen ende
        • z.B. Zeile 4
        • stammt eigentlich aus C++
      • mehrzeiliger Kommentar
        • alles zwischen /* (Anfang) und */ (Ende)
        • z.B. Zeile 6--9
        • darf nicht geschachtelt werden!
        • d.h. /* ... /* ... */ ... */ ist Syntaxfehler
    • Vorschlag

      • Verwende // für echte Kommentare
      • Verwende /* ... */ zum Debuggen
    In [23]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    #include <stdio.h>
    
    int main() {
      // printf("1 ");
      printf("2 ");
      /*
        printf("3");
        printf("4");
      */
      printf("5");
      printf("\n");
    }
    
    2 5
    

    Beispiel zu vernünftigem Kommentieren von Code¶

    In [24]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // Exercise 0.1 -- how to comment a program
    // author: Michael Feischl
    // last modified: 11.03.2020
    
    #include <stdio.h>
    
    double min(double, double);
    
    int main() {
      double x = 0;
      double y = 0;
    
      // read input x
      printf("Input x = ");
      scanf("%lf",&x);
      
      // read input y
      printf("Input y = ");
      scanf("%lf",&y);
      
      // determine and print minimum
      printf("min(x,y) = %f\n",min(x,y));
    }
    
    // The function min determines the mininum of two
    // double values x and y
    
    double min(double x, double y) {
      if (x > y) {
        // if x > y, then min(x,y) = y
        return y;
      }
      else {
        // otherwise, x <= y and min(x,y) = x
        return x;
      }
    }
    
    Input x = 3
    Input y = 4
    min(x,y) = 3.000000
    
    In [27]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // author: Dirk Praetorius
    // last modified: 15.03.2020
    
    #include <stdio.h>
    
    // The Euclidean algorithm to compute the greatest
    // common divisor (gcd) is based on gcd(a,b) = gcd(a-b,b)
    // for a>b and gcd(a,b) = gcd(b,a).
    
    int euclid(int a, int b) {
      int tmp = 0;
    
      // iteration gcd(a,b) = gcd(a-b,b), realized
      // using division with remainder, until b = 0.
      // Then, since it was a==b, it holds gcd = a.
    
      while (b != 0) {
        tmp = a%b;
        a = b;
        b = tmp;
      }
    
      return a;
    }
    
    int main(){
      int a = 200;
      int b = 110;
    
      printf("gcd(%d,%d)=%d\n",a,b,euclid(a,b));
    }
    
    gcd(200,110)=10
    

    Rekursionen¶

    Rekursive Funktion¶

    • Funktion ist rekursiv, wenn sie sich selber aufruft
    • natürliches Konzept in der Mathematik:
      • n!=n⋅(n−1)!n!=n⋅(n−1)!
    • d.h. Rückführung eines Problems auf einfacheres Problem derselben Art
    • Achtung:
      • Rekursion darf nicht endlos sein
      • d.h. Abbruchbedingung für Rekursion ist wichtig
        • häufig Schleifen statt Rekursion möglich (später!)
      • idR. Rekursion eleganter
      • idR. Schleifen effizienter

    Beispiel: Faktorielle¶

    In [28]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #include <stdio.h>
    
    // The function factorial computes the factorial
    // of a nonnegative integer n recursively
    
    int factorial(int n) {
      if (n <= -1) {
        // factorial not defined for n <= -1
        return -1;
      } 
      else {
        if (n > 1) {
          // recursive step n! = n*(n-1)!
          return n*factorial(n-1);
        }
        else {
          // stopping criterion 0! = 1 = 1!
          return 1;
        }
      }
    }
    
    int main() {
      int n = 0;
      int nfac = 0;
      printf("n=");
      scanf("%d",&n);
      nfac = factorial(n);
      if (nfac <= 0) {
        printf("Wrong input!\n");
      }
      else {
        printf("%d!=%d\n",n,nfac);
      }
    }
    
    n=5
    5!=120
    

    Reihenfolge der Selbstaufrufe am Beispiel Faktorielle¶

      • Die blauen Pfeile symbolisieren die Funktionsaufrufe (oben) und die Rückgaben (unten).
      • Die grauen Pfeile verdeutlichen, dass es sich oben und unten um die selbe Funktion handelt, aber der Selbstaufruf durch den Rückgabewert ersetzt wird.

    Nullstellensuche mit dem Bisektionsverfahren¶

    • Gegeben: stetiges f:[a,b]→Rf:[a,b]→R mit f(a)f(b)≤0f(a)f(b)≤0

      • Toleranz τ>0τ>0
    • Tatsache: Zwischenwertsatz ⇒⇒ mind. eine Nst

      • denn f(a)f(a) und f(b)f(b) haben versch. Vorzeichen
    • Gesucht: x0∈[a,b]x0∈[a,b] mit folgender Eigenschaft

      • ∃˜x0∈[a,b]f(˜x0)=0∃x~0∈[a,b]f(x~0)=0 und |x0−˜x0|≤τ|x0−x~0|≤τ
    • Bisektionsverfahren = iterierte Intervallhalbierung

      • Solange wie Intervallbreite |b−a|>2τ|b−a|>2τ
      • Berechne Intervallmittelpunkt mm und f(m)f(m)
      • Falls f(a)f(m)≤0f(a)f(m)≤0, betrachte Intervall [a,m][a,m]
      • sonst betrachte halbiertes Intervall [m,b][m,b]
      • x0:=mx0:=m ist schließlich gesuchte Approximation
    • Verfahren basiert nur auf Zwischenwertsatz
    • Terminiert nach endlich vielen Schritten, da jeweils Intervall halbiert wird

    • Konvergenz gegen Nst. ˜x0x~0 für τ=0τ=0.

    Illustration des Bisektionsverfahrens¶

    Beispiel: Bisektionsverfahren¶

    In [29]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #include <stdio.h>
    
    double f(double x) {
      return x*x + 1/(2 + x) - 2;
    }
    
    double bisection(double a, double b, double tol){
      double m = 0.5*(a+b);
      if ( b - a <= 2*tol ) {
        return m;
      }
      else {
        if ( f(a)*f(m) <= 0 ) {
          return bisection(a,m,tol);
        }
        else {
          return bisection(m,b,tol);
        }
      }
    }
    
    int main() {
      double a = 0;
      double b = 10;
      double tol = 1e-12;
      double x = bisection(a,b,tol);
      
      printf("Approximate zero of the function x=%g\n",x);
      printf("Function value f(x)=%g\n",f(x));  // %g chooses good floating point output
    }
    
    Approximate zero of the function x=1.30278
    Function value f(x)=-5.64659e-13
    

    Mathematische Funktionen¶

    • Deklaration der math. Funktionen in math.h

      • Input & Output der Funktionen sind vom Typ double
    • Wenn diese Funktionen benötigt werden

      • im Source-Code:#include <math.h>
      • Compilieren des Source-Code mit zusätzlicher Linker-Option -lm, d.h. gcc file.c -o output -lm erzeugt Executable output
        • moderne Compiler linken oft automatisch, d.h., -lm nicht nötig
    • Diese Bibliothek stellt u.a. zur Verfügung

      • Trigonometrische Funktionen
      • cos, sin, tan, acos, asin, atan, cosh, sinh, tanh
      • Exponentialfunktion und Logarithmus
      • exp, log, log10
      • Potenz- und Wurzelfunktion
      • pow, sqrt (wobei xy=pow(x,y)xy=pow(x,y))
      • Häufig ist explizite Berechnung effizienter:
      • BSP: x3x3 mittels x * x * x berechnen
      • BSP: (−1)n(−1)n über if ... else berechnen: (−1)n=1(−1)n=1 falls nn gerade, sonst (−1)n=−1(−1)n=−1
      • Absolutbetrag fabs
      • Rundung auf ganze Zahlen: round, floor, ceil
    • ACHTUNG: In der Bibliothek stdlib.h gibt es abs
      • abs ist Absolutbetrag für int (ggf. type cast!)
      • fabs ist Absolutbetrag für double

    Preprocessor, Compiler & Linker¶

    • Ein Compiler besteht aus mehreren Komponenten, die nacheinander abgearbeitet werden
    • Preprocessor wird intern gestartet, bevor der Source-Code compiliert wird

      • Ersetzt Text im Code durch anderen Text
      • Preprocessor-Befehle beginnen immer mit # und enden nie mit Semikolon, z.B.
      • #define text replacement
        • in allen nachfolgenden Zeilen wird der Text text durch replacement ersetzt
        • zur Definition von Konstanten
        • Konvention: GROSS_MIT_UNDERSCORES
      • #include file
        • einfügen der Datei file
    • Compiler übersetzt (Source-)Code in Object-Code

      • Object-Code = Maschinencode, bei dem symbolische Namen (z.B. Funktionsnamen) noch vorhanden sind
    • Weiterer Object-Code wird zusätzlich eingebunden

      • z.B. Bibliotheken (= Sammlungen von Funktionen)
    • Linker ersetzt symbolische Namen im Object- Code durch Adressen und erstellt dadurch ein ausführbares Programm, sog. Executable

    Bibliotheken & Header-Files¶

    • (Funktions-) Bibliothek (z.B. math. Funktionen) besteht immer aus 2 Dateien

      • Object-Code
      • zugehöriges Header-File
    • Im Header-File steht die Deklaration aller Fktn, die in der Bibliothek vorhanden sind

    • Will man Bibliothek verwenden, muss man zugehöriges Header-File einbinden

      • #include header bindet Header-File header aus Standardverzeichnis /usr/include/ ein,
      • z.B. math.h (Header-File zur math. Bib.)
      • #include "datei" bindet Datei aus aktuellem Verzeichnis ein (z.B. Downloads vom Internet)
      • idR. führt CC-Compiler #include <stdio.h>von allein aus (in zugehöriger Bib. liegt z.B. printf)
    • Ferner muss man den Object-Code der Bibliothek hinzulinken

      • Wo Object-Code der Bibliothek liegt, muss gcc mittels Option -l (und -L) mitgeteilt werden
      • z.B. gcc file.c -lm linkt Mathematik Bibliothek (Dateiname libm.a)
      • Standardbibliotheken automatisch gelinkt, z.B. stdio (also keine zusätzliche Option nötig)

    Elementares Beispiel¶

    • Preprocessor-Befehle in 1, 2 ohne Semikolon
    • Compilieren mit gcc sqrt.c -lm
    • Vergisst man -lm ⇒⇒ Fehlermeldung des Linkers

    In function 'main' sqrt.c:(.text+0x24): undefined reference to 'sqrt' collect2: ld returned 1 exit status

    In [36]:
    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>
    #include <math.h>
    
    int main() {
      double x = 2.;
      double y = sqrt(x);
      printf("sqrt(%f)=%f\n",x,y);
    }
    
    sqrt(2.000000)=1.414214
    

    Code mehrfach ausführen mit Schleifen¶

    Schleifen¶

    • Schleifen führen einen oder mehrere Befehle wiederholt aus
    • In Aufgabenstellung häufig Hinweise, wie
      • Vektoren & Matrizen
      • Laufvariablen j=1,…,nj=1,…,n
      • Summen ∑nj=1aj:=a1+a2+⋯+an∑j=1naj:=a1+a2+⋯+an
      • Produkte ∏nj=1aj:=a1⋅a2⋯an∏j=1naj:=a1⋅a2⋯an
      • Text wie z.B. solange bis oder solange wie
    • Man unterscheidet
      • Zählschleifen for: Wiederhole etwas eine gewisse Anzahl oft
      • Bedingungsschleifen while, do...while: Wiederhole etwas solange, wie eine Bdg. gilt oder bis eine Bedingung gilt

    Die while-Schleife¶

    • Formal: while( condition ) statement

      • "Solange wie etwas gilt, tue Folgendes:"
    • Vor jedem Durchlauf wird condition geprüft & Abbruch, falls nicht erfüllt

      • sog. kopfgesteuerte Schleife
    • Eventuell also kein einziger Durchlauf!
    • statement kann Block sein
    In [37]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    #include <stdio.h>
    
    int main() {
      int counter = 9;
    
      while (counter > 0) {
        printf("%d ",counter);
        counter = counter-1;
      }
      printf("\n");
    }
    
    9 8 7 6 5 4 3 2 1
    

    Operator ++¶

    • ++a und a++ sind arithmetisch äquivalent zu a=a+1
    • Zusätzlich aber Auswertung von Variable a
    • Präinkrement ++a
      • Erst erhöhen, dann auswerten
    • Postinkrement a++
      • Erst auswerten, dann erhöhen
    In [38]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #include <stdio.h>
    
    int main() {
      int a = 0;
      int b = 43;
    
      printf("1) a=%d, b=%d\n",a,b);
    
      b = a++;
      printf("2) a=%d, b=%d\n",a,b);
    
      b = ++a;
      printf("3) a=%d, b=%d\n",a,b);
    }
    
    1) a=0, b=43
    2) a=1, b=0
    3) a=2, b=2
    

    Operatoren ++ und --¶

    • Analog zu a++ und ++a gibt es

      • Prädekrement --a
        • Erst verringern, dann auswerten
      • Postdekrement a--
        • Erst auswerten, dann verringern
    • Beachte Unterschied in Bedingungsschleife!

    In [39]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    #include <stdio.h>
    
    int main() {
      int counter = 5;
    
      while (--counter > 0) {
        printf("%d ",counter);
      }
      printf("\n");
    }
    
    4 3 2 1
    

    Bisektionsverfahren (Wiederholung)¶

    • Gegeben: stetiges f:[a,b]→Rf:[a,b]→R mit f(a)f(b)≤0f(a)f(b)≤0

      • Toleranz τ>0τ>0
    • Tatsache: Zwischenwertsatz ⇒⇒ mind. eine Nst

      • denn f(a)f(a) und f(b)f(b) haben versch. Vorzeichen
    • Gesucht: x0∈[a,b]x0∈[a,b] mit folgender Eigenschaft

      • ∃˜x0∈[a,b]f(˜x0)=0∃x~0∈[a,b]f(x~0)=0 und |x0−˜x0|≤τ|x0−x~0|≤τ
    • Bisektionsverfahren = iterierte Intervallhalbierung

      • Solange wie Intervallbreite |b−a|>2τ|b−a|>2τ
      • Berechne Intervallmittelpunkt mm und f(m)f(m)
      • Falls f(a)f(m)≤0f(a)f(m)≤0, betrachte Intervall [a,m][a,m]
      • sonst betrachte halbiertes Intervall [m,b][m,b]
      • x0:=mx0:=m ist schließlich gesuchte Approximation
    • Verfahren basiert nur auf Zwischenwertsatz
    • Terminiert nach endlich vielen Schritten, da jeweils Intervall halbiert wird

    • Konvergenz gegen Nullstelle ˜x0x~0 für τ=0τ=0.

    Bisektionsverfahren (mit Schleife statt Rekursion)¶

    • Verwendung von Variablen fa und fm vermeidet doppelte Funktionsauswertung
    • Schleife ist idR. effizienter als Rekursion!
    In [40]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #include <stdio.h>
    #include <math.h>
    
    double f(double x) {
      return x*x + 1/(2 + x) - 2;
    }
    
    double bisection(double a, double b, double tol){
      double fa = f(a);
      double m = 0.5*(a+b);
      double fm = 0;
    
      while ( b - a > 2*tol ) {
        m = 0.5*(a+b);
        fm = f(m);
        if ( fa*fm <= 0 ) {
          b = m;
        }
        else {
          a = m;
          fa = fm;
        }
      }
      return m;
    }
    
    int main() {
      double a = 0;
      double b = 10;
      double tol = 1e-12;
      double x = bisection(a,b,tol);
      
      printf("Approximate zero of the function x=%g\n",x);
      printf("Function value f(x)=%g\n",f(x));
    }
    
    Approximate zero of the function x=1.30278
    Function value f(x)=-1.99352e-12
    

    Euklidischer Algorithmus¶

    • Gegeben: zwei ganze Zahlen a,b∈Na,b∈N
    • Gesucht: größter gemeinsamer Teiler gcd(a,b)∈Ngcd(a,b)∈N

      • engl. greatest common divisor (gcd)
    • Euklidischer Algorithmus:

      • Falls a=ba=b, gilt gcd(a,b)=agcd(a,b)=a
      • Vertausche aa und bb, falls a<ba<b
      • Dann gilt gcd(a,b)=gcd(a−b,b)gcd(a,b)=gcd(a−b,b), denn:
        • Sei gg Teiler von a,ba,b
        • d.h. ga0=aga0=a und gb0=bgb0=b mit a0,b0∈Na0,b0∈N, g∈Ng∈N
        • also g(a0−b0)=a−bg(a0−b0)=a−b und a0−b0∈Na0−b0∈N
        • d.h. gg teilt bb und a−ba−b
        • d.h. gcd(a,b)≤gcd(a−b,b)gcd(a,b)≤gcd(a−b,b)
        • analog gcd(a−b,b)≤gcd(a,b)gcd(a−b,b)≤gcd(a,b)
        • Ersetze aa durch a−ba−b, wiederhole diese Schritte
    • Erhalte gcd(a,b)gcd(a,b) nach endlich vielen Schritten:
      • Falls a≠ba≠b, wird also n:=max{a,b}∈Nn:=max{a,b}∈N pro Schritt um mindestens 11 kleiner
      • Nach endlichen vielen Schritten kann also nicht mehr a≠ba≠b gelten
      • d.h. Algorithmus terminiert

    Illustration des Euklidischen Algorithmus¶

      • Wir berechnen gcd(6,14)=2gcd(6,14)=2 mit dem Euklidischen Algorithms
      • Die grauen Linien verdeutlichen, dass die Zwischenergebnisse immer den selben größten gemeinsamen Teiler haben

    Euklidischer-Algorithmus¶

    • berechnet gcd von a,b∈Na,b∈N
    • basiert auf gcd(a,b)=gcd(a−b,b)gcd(a,b)=gcd(a−b,b) für a>ba>b
    • Für a=ba=b terminiert while mit gcd(a,b)=a=bgcd(a,b)=a=b
    In [41]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>
    
    int main() {
      int a = 200;
      int b = 110;
      int tmp = 0;
    
      printf("gcd(%d,%d)=",a,b);
      
      while (a != b) {
        if (a < b) {
          tmp = a;
          a = b;
          b = tmp;
        }
        a = a-b;
      }
      
      printf("%d\n",a);
    }
    
    gcd(200,110)=10
    

    Euklid-Algorithmus (verbessert)¶

    • Kernstück des Euklid-Algorithmus
    In [ ]:
    1
    2
    3
    4
    5
    6
    7
    8
    while (a != b) {
        if (a < b) {
          tmp = a;
          a = b;
          b = tmp;
        }
        a = a-b;
      }
    
    • Erinnerung: a%b ist Divsionsrest von a/b
    • Euklid-Algorithmus iteriert a=a-b bis a≤ba≤b
      • d.h. bis a=a%b
      • falls fertig, gilt a=0 und Ergebnis b ist größter gemeinsamer Teiler
    In [ ]:
    1
    2
    3
    4
    5
    6
    7
    8
    while (a != 0) {
        if (a < b) {
          tmp = a;
          a = b;
          b = tmp;
        }
        a = a%b;
      }
    
    • Divisionsrest erfüllt immer a%b < b
      • d.h. es wird immer vertauscht nach Rechnung
      • falls fertig, gilt b=0 und Ergebnis a ist größter gemeinsamer Teiler
    In [ ]:
    1
    2
    3
    4
    5
      while (b != 0) {
        tmp = a%b;
        a = b;
        b = tmp;
      }
    

    Beispiel: Euklidischer Algorithmus (verbessert)¶

    In [45]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    
    int main() {
      int a = 200;
      int b = 110;
      int tmp = 0;
    
      printf("ggT(%d,%d)=",a,b);
    
      while (b != 0) {
        tmp = a%b;
        a = b;
        b = tmp;
      }
      
      printf("%d\n",a);
    }
    
    ggT(200,110)=10
    

    Die do-while-Schleife¶

    • Formal: do statement while( condition );

      • "Tue Folgendes, solange wie etwas gilt:"
    • Nach jedem Durchlauf wird condition geprüft & Abbruch, falls nicht erfüllt

      • sog. fußgesteuerte Schleife
    • Also mindestens ein Durchlauf!
    • statement kann Block sein
    In [46]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    #include <stdio.h>
    
    int main() {
      int counter = 5;
    
      do {
        printf("%d ",counter);
      }
      while (--counter>0);
      printf("\n");
    }
    
    5 4 3 2 1
    

    Ein weiteres Beispiel¶

    • Fibonacci-Folge strebt gegen unendlich

      • x0:=0x0:=0, x1:=1x1:=1 und xn+1:=xn−1+xnxn+1:=xn−1+xn für n∈Nn∈N
    • Ziel: Berechne erstes Folgenglied mit xn>cxn>c für gegebene Schranke c∈Nc∈N

    In [47]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>
    
    int main() {
      int x0 = 0;
      int x1 = 1;
      int tmp = 0;
      int c = 0;
      
      printf("c=");
      scanf("%d",&c);
      
      printf("%d %d ",x0,x1);
    
      do {
        tmp = x0 + x1;
        x0 = x1;
        x1 = tmp;
        printf("%d ",x1);
      }
      while(x1 <= c);
    
      printf("\n");
    }
    
    c=5
    0 1 1 2 3 5 8
    

    "solange wie" vs. "solange bis"¶

    • break beendet die aktuelle Schleife

    • while hat Laufbedingung condition

      • d.h. Schleife läuft, solange wie condition wahr
    • Algorithmen haben idR. Abbruchbedingung done

      • d.h. Abbruch, falls done wahr
      • d.h. condition == Negation von done
    • einfache Realisierung über Endlosschleife mit break

      • Bedingung in Zeile 11 ist immer wahr!
      • Abbruch erfolgt nur durch break in Zeile 13
    In [48]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>
    
    int main() {
      int a = 200;
      int b = 110;
      int tmp = 0;
      
      printf("gcd(%d,%d)=",a,b);
      
      // Euclidean algorithm with infinite loop + break
      while (1) {
        if (b == 0) {
          break;
        }
        else {
          tmp = a%b;
          a = b;
          b = tmp;
        }
      }
      
      printf("%d\n",a);
    }
    
    gcd(200,110)=10
    

    Einfache Fehlerkontrolle¶

    Motivation¶

    • Fakt ist: alle Programmierer machen Fehler
      • Code läuft beim ersten Mal nie richtig
    • Großteil der Entwicklungszeit geht in Fehlersuche
    • "Profis" unterscheiden sich von "Anfängern" im Wesentlichen durch effizientere Fehlersuche
    • Syntax-Fehler sind leicht einzugrenzen
      • es steht Zeilennummer dabei (Compiler!)
      • Tipp: Verwende während des Programmierens zum Syntax-Test regelmäßig (Details später!)
        • gcc -c name.c nur Objekt-Code
        • gcc -c -Wall name.c alle Warnungen
    • Laufzeitfehler sind viel schwieriger zu finden
      • Programm läuft, tut aber nicht das Richtige
      • manchmal fällt der Fehler ewig nicht auf ⇒⇒ sehr schlecht

    Fehler vermeiden!¶

    • Programmier-Konventionen beachten
      • z.B. bei Namen für Variablen, Funktionen etc.
    • Kommentarzeilen dort, wo im Code etwas passiert
      • z.B. Verzweigung mit nicht offensichtlicher Bdg.
      • z.B. Funktionen (Zweck, Input, Output)
    • jede Funktion hat nur eine Funktionalität

      • jede Funktion einzeln & sofort testen
      • Wenn später Funktion verwendet wird, kann ein etwaiger Fehler dort nicht mehr sein!
        • d.h. kann Fehler im Programm schneller lokalisieren!
    • jede Funktionalität hat eigene Funktion

      • Programm in überschaubare Funktionen zerlegen!
    • nicht alles auf einmal programmieren!
      • Achtung: Häufiger Anfängerfehler!
    • Möglichst viele Fehler bewusst abfangen!
      • Funktions-Input auf Konsistenz prüfen!
        • Fehler-Abbruch, falls inkonsistent!
      • garantieren, dass Funktions-Output zulässig!

    Bibliothek assert.h¶

    • Ziel: Sofortabbruch mit Fehlermeldung, sobald Funktion merkt, dass Input / Output unzulässig
    • #include <assert.h>
      • assert(condition)liefert Fehlerabbruch, falls condition falsch
        • mit Ausgabe der Zeilennummer im Source-Code
    In [49]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>
    #include <assert.h>
    
    void test(int x, int y) {
      assert(x<y);
      printf("It holds x < y\n");
    }
    
    int main() {
      int x = 0;
      int y = 0;
      
      printf("x = ");
      scanf("%d",&x);
      
      printf("y = ");
      scanf("%d",&y);
      
      test(x,y);
    }
    
    x = 6
    y = 3
    
    tmp0re3hp7h.out: /tmp/tmpdnqxpqyp.c:5: test: Assertion `x<y' failed.
    [C kernel] Executable exited with code -6

    Beispiel: Euklidischer Algorithmus¶

    • assert stellt sicher, dass Input zulässig
      • d.h. a,b∈Na,b∈N ist notwendig!
    In [50]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #include <stdio.h>
    #include <assert.h>
    // The Euclidean algorithm to compute the greatest
    // common divisor (gcd) is based on gcd(a,b) = gcd(a-b,b)
    // for a>b and gcd(a,b) = gcd(b,a).
    
    int euclid(int a, int b) {
      assert(a > 0);
      assert(b > 0);
      int tmp = 0;
    
      // iteration gcd(a,b) = gcd(a-b,b), realized
      // using division with remainder, until b = 0.
      // Then, since it was a==b, it holds gcd = a.
    
      while (b != 0) {
        tmp = a%b;
        a = b;
        b = tmp;
      }
    
      return a;
    }
    
    int main(){
        int a=0,b=0;
        printf("Input:\na=");
        scanf("%d",&a);
        printf("b=");
        scanf("%d",&b);
        printf("gcc(%d,%d) = %d\n",a,b,euclid(a,b));
    }
    
    Input:
    a=7
    b=5
    gcc(7,5) = 1
    

    Testen von Code¶

    Motivation¶

    • Ariane 5 Explosion ('96)
      • Konversion double →→ int
      • Schaden ca. 500500 Mio. Dollar
    • Patriot Missile Fehler, Golfkrieg ('91)
      • Zeitmessung falsch berechnet & Rundungsfehler
    • Untergang Sleipner A offshore Plattform ('91)
      • Fehler in der Finite-Elemente-Berechnung
      • Kosten ca 600600 Mio. EUR
    • "kleine BUGs, große GAUs"
      • http://www5.in.tum.de/~huckle/bugs.html

    Qualitätssicherung¶

    • Software entsteht durch menschliche Hand
    • Fehler zu machen, ist menschlich!
    • Software wird deshalb Fehler enthalten
    • Ziel: (Laufzeit-) Fehler finden vor großem Schaden
    • Je später Fehler entdeckt werden, desto aufwändiger ist ihre Behebung!
    • Schon beim Implementieren auf Qualität achten
      • siehe oben: Fehler vermeiden!
    • wünschenswert: je 1/3 Zeit für
      • Programmieren
      • Testen
      • Dokumentieren
    • wünschenswert: Dokumentation der Tests!
      • damit reproduzierbar
    • In der Praxis meist viel Programmieren, wenig Testen, noch weniger Dokumentieren ;-)

    Testen¶

    • Testen ist der Prozess, ein Programm mit der Absicht auszuführen, Fehler zu finden!

      • Glenford Myers: Art of Software Testing (1979)
    • Test ist der Vergleich des Verhaltens eines Prg (Ist) mit dem erwarteten Verhalten eines Systems (Soll)

    • Es ist praktisch nicht möglich, alle Programmfunktionen und
      alle möglichen Werte in den Eingabedaten in allen Kombinationen zu testen.

      • d.h. Tests sind idR. unvollständig!
    • Probleme beim unvollständigen Testen
      • Tests erlauben nur das Auffinden von Fehlern
      • Tests können Korrektheit nicht beweisen
      • Fehlerursache ist durch Soll-Ist-Vergleich nicht zwangsläufig klar
        • Testfälle können selbst fehlerhaft sein!
    • Vorteile beim unvollständigen Testen
      • Zeitaufwand vertretbar
      • Tests beziehen sich idR. auf "realistischen Input"
      • Tests sind idR. reproduzierbar

    Arten von Tests¶

    • strukturelle Tests (für jede Funktion)

      • Werden alle Anweisungen ausgeführt oder gibt es toten Code?
      • Treten Fehler auf, wenn if ... else mit wahr / falsch durchlaufen werden?
    • funktionale Tests (für jede Fkt. und Programm)

      • Tut jede Funktion mit zulässigen Parametern das Richtige? (d.h. Ergebnis korrekt?)
      • Tut das Programm (bzw. Teilabschnitte) das Richtige? (d.h. Ergebnis korrekt?)
      • Werden unzulässige Parameter erkannt?
      • Werden Grenzfälle / Sonderfälle korrekt erkannt und liefern das Richtige?
      • Was passiert bei Fehleingaben, d.h. bei Fehler des Benutzers?

    Funktionale Tests?¶

    • Ziel: Tut Funktion / Programm das Richtige?
    • funktionale Tests brauchen Testfälle
      • mit bekanntem Ergebnis / Output
    • Was sind generische Fälle / Parameter?
      • Bei welchen Fällen treten Verzweigungen auf?
      • Möglichst viele Verzweigungen abdecken!
    • Welche Fälle sind kritisch?
      • Zahlen werden im Rechner nicht exakt dargestellt (später mehr!)
      • Außerdem keine exakte Arithmetik bei double!
        • jede double-Rechnung hat Rechenfehler!
      • Wo können aufgrund solcher Rechenfehler andere Ergebnisse auftreten?
        • z.B. Ist ein Punkt auf dem Kreisrand?
    • früh mit dem Testen beginnen
      • nach Implementierung jeder Funktion!
      • nicht erst dann, wenn Prg komplett fertig!
    • nach Code-Korrektur alle(!) Tests wiederholen
      • deshalb Dokumentation der Tests!
    • Ab jetzt in der UE stets: Wie wurde getestet?
      • allerdings nur inhaltlich
      • d.h. ohne Fehleingaben des Nutzers

    Daten speichern in Arrays¶

    Vektoren sind 1D-Arrays¶

    • Deklaration eines Vektors x=(x0,…,xN−1)∈RNx=(x0,…,xN−1)∈RN:

      • double x[N]; →→ x ist double-Vektor
    • Zugriff auf Komponenten:

      • x[j] entspricht xjxj
      • Jedes x[j] ist vom Typ double
    • Analoge Deklaration für andere Datentypen

      • int y[N]; →→ y ist int-Vektor
    • ACHTUNG mit der Indizierung der Komponenten
      • Indizes 0,…,N−10,…,N−1 in CC
      • idR. Indizes 1,…,N1,…,N in Mathematik
    • Initialisierung bei Deklaration möglich:
      • double x[3] = {1,2,3}; deklariert den Vektor x=(1,2,3)∈R3x=(1,2,3)∈R3
    • Vektor-Initialisierung nur bei Deklaration erlaubt
      • Später zwingend komponentenweises Schreiben!
        • d.h. x[0] = 1; x[1] = 2; x[2] = 3; ist OK!
        • x = {1,2,3} ist verboten!

    Beispiel: Einlesen eines Vektors¶

    • Ausgabe double über printf mit Platzhalter %f

    • Einlesen double über scanf mit Platzhalter %lf

    • Es gibt keine Möglichkeit, Vektoren als Ganzes einzulesen / auszugeben!
    In [51]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <stdio.h>
    
    int main() {
      double x[3] = {0,0,0};
      
      printf("Read a vector x in R^3 from keyboard:\n");
      
      // read x0
      printf("x_0 = ");
      scanf("%lf",&x[0]);
    
      // read x1
      printf("x_1 = ");
      scanf("%lf",&x[1]);
    
      // read x2
      printf("x_2 = ");
      scanf("%lf",&x[2]);
    
      // print vector (componentwise)
      printf("x = (%f, %f, %f)\n",x[0],x[1],x[2]);
    }
    
    Read a vector x in R^3 from keyboard:
    x_0 = 3
    x_1 = 4
    x_2 = 5
    x = (3.000000, 4.000000, 5.000000)
    

    Achtung: Statische Arrays¶

    • Die Länge von Arrays ist statisch
      • nicht veränderbar während Programmablauf
      • x∈R3x∈R3 kann nicht zu x∈R5x∈R5 erweitert werden
    • Programm kann nicht selbständig herausfinden, wie groß ein Array ist
      • d.h. Programm weiß bei Ablauf nicht, dass Vektor x∈R3x∈R3 Länge 33 hat
      • Muss bei der Programmierung beachtet werden!
    • Achtung mit Indizierung!
      • Indizes laufen 0,…,N−10,…,N−1 in C
      • Prg kann nicht wissen, ob x[j] definiert ist
        • x muss mindestens Länge j+1j+1 haben!
        • falsche Indizierung ist kein Syntaxfehler!
        • sondern ggf. ein Laufzeitfehler!
    • Arrays dürfen nicht Output einer Funktion sein!
      • Das ist auch kein Problem, denn:
    • Arrays werden mit Call by Reference an eine Funktion übergeben!
      • Erklärung folgt später (→→ Pointer!)

    Arrays & Call by Reference¶

    • Man sieht: Call by Reference bei Vektoren!

      • Erklärung folgt später (→→ Pointer!)
    • in Funktionssignatur darf man Vektorlänge weglassen

      • Programmierer für korrekten Zugriff zuständig!
      • void allByReference(double y[]) auch OK!
    In [52]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <stdio.h>
    
    void callByReference(double y[3]) {
      printf("a) y = (%f, %f, %f)\n",y[0],y[1],y[2]);
      y[0] = 1;
      y[1] = 2;
      y[2] = 3;
      printf("b) y = (%f, %f, %f)\n",y[0],y[1],y[2]);
    }
    
    int main() {
      double x[3] = {0,0,0};
      
      printf("c) x = (%f, %f, %f)\n",x[0],x[1],x[2]);
      
      // function call modifies the coefficients!
      callByReference(x);
      printf("d) x = (%f, %f, %f)\n",x[0],x[1],x[2]);
    }
    
    c) x = (0.000000, 0.000000, 0.000000)
    a) y = (0.000000, 0.000000, 0.000000)
    b) y = (1.000000, 2.000000, 3.000000)
    d) x = (1.000000, 2.000000, 3.000000)
    

    Falsche Indizierung von Vektoren¶

    • Zeile 7, 10: Falscher Zugriff auf Vektor x,

      • d.h. Laufzeitfehler
        • ggf. werden andere Daten überschrieben!
      • Trotzdem keine Fehlermeldung/Warnung vom Compiler!
      • Für korrekte Indizes sorgt der Programmierer!
    • Bei kleinen Überschreitungen der Arraylänge oft keine Fehlermeldung. Wenn man aber 100 mit 10000 ersetzt, dann stürzt das Programm ab.

      • weil Betriebssystem merkt, dass das Programm "aus dem Ruder läuft"!
      • d.h. Programm greift auf Speicherbereiche zu, die ihm nicht zugeordnet / erlaubt sind.
    In [53]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    #include <stdio.h>
    
    int main() {
      int x[3] = {0,1,2};
      
      // this coefficient has no reserved memory!
      x[100] = 43;
    
      // nevertheless, writing and reading is possible!
      printf("x = (%d, %d, %d), x[%d] = %d\n", x[0],x[1],x[2],100,x[100]);
    }
    
    x = (0, 1, 2), x[100] = 43
    

    Matrizen¶

    • Matrix A∈RM×NA∈RM×N ist rechteckiges Schema
    A=⎛⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜⎝A00A01A02…A0,N−1A10A11A12…A1,N−1A20A21A22…A2,N−1⋮⋮⋮⋮AM−1,0AM−1,1AM−1,2…AM−1,N−1⎞⎟ ⎟ ⎟ ⎟ ⎟ ⎟ ⎟⎠A=(A00A01A02…A0,N−1A10A11A12…A1,N−1A20A21A22…A2,N−1⋮⋮⋮⋮AM−1,0AM−1,1AM−1,2…AM−1,N−1)

    mit Koeffizienten Ajk∈RAjk∈R

    • zentrale math. Objekte der Linearen Algebra
    • Deklaration einer Matrix A∈RM×NA∈RM×N:

      • double A[M][N];
    • Zugriff auf Komponenten:

      • A[j][k] entspricht AjkAjk
      • Jedes A[j][k] ist vom Typ double
    • zeilenweise Initialisierung bei Deklaration möglich:

      • double A[2][3] = {{1,2,3},{4,5,6}}; deklariert + initialisiert A=(123456)A=(123456)
      • Nur bei gleichzeitiger Deklaration erlaubt, vgl. Vektoren

    Allgemeine Arrays¶

    • Vektor ist ein 11-dim. Array
    • Matrix ist ein 22-dim. Array
    • Ist type Datentyp, so deklariert
      • type x[N]; einen Vektor der Länge NN
      • Koeffizienten x[j] sind Variablen vom Typ type
    • Ist type Datentyp, so deklariert
      • type x[M][N]; eine M×NM×N Matrix
      • x[j] ist Vektor vom Typ type (der Länge NN)
      • Koeff. x[j][k] sind Variablen vom Typ type
    • Auch mehr Indizes möglich
      • type x[M][N][P]; deklariert 33-dim. Array
      • x[j] ist N×PN×P Matrix vom Typ type
      • x[j][k] ist Vektor vom Typ type (der Länge PP)
      • Koeff. x[j][k][p] sind Variablen vom Typ type
    • etc.
    • Arrays dürfen nicht Output einer Funktion sein!
    • Arrays werden mit Call by Reference an eine Funktion übergeben!

    Zählschleifen¶

    Die for-Schleife¶

    • for (init.; cond.; step-expr.) statement
    • Ablauf einer for-Schleife
      • (1) Ausführen der Initialisierung init.
      • (2) Abbruch, falls Bedingung cond. nicht erfüllt
      • (3) Ausführen von statement
      • (4) Ausführen von step-expr.
      • (5) Sprung nach (2)
    • statement ist

      • entweder eine logische Programmzeile
      • oder mehrere Programmzeilen als Block {...},
    • in C ist for eigentlich eine Bedingungsschleife

      • trotzdem immer for verwenden, um Statement "eine fixe Anzahl" oft zu wiederholen
      • gut programmiert = leicht verständlicher Code!
    In [54]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    #include <stdio.h>
    
    int main() {
      int j = 0;
    
      for (j=5; j>0 ; j--) {
        printf("%d ",j);
      }
    
      printf("\n");
    }
    
    5 4 3 2 1
    

    Vektor einlesen & ausgeben¶

    • Funktionen müssen Länge von Arrays kennen!
      • d.h. zusätzlicher Input-Parameter nötig
    • Arrays werden mit Call by Reference übergeben!
    In [55]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #include <stdio.h>
    
    // Read from the keyboard a double vector
    // of length dim
    
    void scanVector(double input[], int dim) {
      int j = 0;
      // iterate over all indices of the vector
      for (j=0; j<dim; j++) {
        // initialize j-th coefficient
        input[j] = 0;
        // read j-th coefficient
        printf("%d: ",j);
        scanf("%lf",&input[j]);
      }
    }
    
    // Print to the screen a double vector
    // of length dim
    
    void printVector(double output[], int dim) {
      int j = 0;
      // iterate over all indices of the vector
      for (j=0; j<dim; j++) {
        // print j-th coefficient
        printf("%f ",output[j]);
      }
      printf("\n");
    }
    
    int main() {
      double x[5];
      // recall: call by reference for arrays
      scanVector(x,5);
      printVector(x,5);
    }
    
    0: 8
    1: 6
    2: 5
    3: 4
    4: 5
    8.000000 6.000000 5.000000 4.000000 5.000000
    

    Minimum eines Vektors¶

    • Hinweise zur Realisierung (vgl. UE)
      • Vektorlänge ist Konstante im Hauptprogramm
        • d.h. Länge im Hauptprogramm nicht veränderbar
      • aber Input z.B. der Funktion scanVector
        • d.h. Funktion arbeitet für beliebige Länge
    In [56]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #include <stdio.h>
    #define DIM 5
    
    void scanVector(double input[], int dim) {
      int j = 0;
      for (j=0; j<dim; j++) {
        input[j] = 0;
        printf("%d: ",j);
        scanf("%lf",&input[j]);
      }
    }
    
    // The function returns the minimum of a double vector
    // of length dim
    
    double min(double input[], int dim) {
      int j = 0;
      // initial guess: the minimum is located at input[0]
      double minval = input[0];
      // iterate over all entries of the vector
      for (j=1; j<dim; j++) {
        // if a smaller value is found, update minimum
        if (input[j] < minval) {
          minval = input[j];
        }
      }
      return minval;
    }
    
    int main() {
      double x[DIM];
      scanVector(x,DIM);
      printf("The minimum of the vector is %f\n", min(x,DIM));
    }
    
    0: 3
    1: 4
    2: 5
    3: 6
    4: 7
    The minimum of the vector is 3.000000
    

    Beispiel: Summensymbol ∑∑¶

    • Berechnung der Summe S=N∑j=1ajS=∑j=1Naj:

      • Abkürzung N∑j=1aj:=a1+a2+⋯+aN∑j=1Naj:=a1+a2+⋯+aN
    • Definiere theoretische Hilfsgröße Sk=k∑j=1ajSk=∑j=1kaj

    • Dann gilt

      • S1=a1S1=a1
      • S2=S1+a2S2=S1+a2
      • S3=S2+a3S3=S2+a3 etc.
    • Realisierung also durch NN-maliges Aufsummieren

      • ACHTUNG: Zuweisung, keine Gleichheit
        • S = a_1
        • S = S + a_2
        • S = S + a_3 etc.

    Beispiel: Summensymbol ∑∑¶

    • Programm berechnet ∑nj=1j∑j=1nj für n=100n=100.

    • ACHTUNG: Bei iterierter Summation nicht vergessen, Ergebnisvariable auf Null zu setzen vgl. Zeile 7

      • Anderenfalls: Falsches/Zufälliges Ergebnis!
    • statt sum = sum + j;

      • Kurzschreibweise sum += j;
    In [57]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #include <stdio.h>
    
    int main() {
      int j = 0;
      int n = 100;
    
      int sum = 0;
    
      for (j=1; j<=n; j++) {
        sum = sum+j;
      }
    
      printf("sum_{j=1}^{%d} j = %d\n",n,sum);
    }
    
    sum_{j=1}^{100} j = 5050
    

    Beispiel: Produktsymbol ∏∏¶

    • Programm berechnet Faktorielle n!=∏nj=1jn!=∏j=1nj für n=5n=5.

    • ACHTUNG: Bei iteriertem Produkt nicht vergessen, Ergebnisvariable auf Eins zu setzen vgl. Zeile 7

      • Anderenfalls: Falsches/Zufälliges Ergebnis!
    • statt factorial = factorial*j;

      • Kurzschreibweise factorial *= j;
    In [58]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #include <stdio.h>
    
    int main() {
      int j = 0;
      int n = 5;
    
      int factorial = 1;
    
      for (j=1; j<=n; j++) {
        factorial = factorial*j;
      }
    
      printf("%d! = %d\n",n,factorial);
    }
    
    5! = 120
    

    Matrix-Vektor-Multiplikation¶

    • Man darf for-Schleifen schachteln
      • Typisches Beispiel: Matrix-Vektor-Multiplikation
    • Seien A∈RM×NA∈RM×N Matrix, x∈RNx∈RN Vektor
    • Def b:=Ax∈RMb:=Ax∈RM durch bj=N−1∑k=0Ajkxkbj=∑k=0N−1Ajkxk
      • Indizierung in C startet bei 00
      • Achtung: In Mathematik Indizierung idR. ab 11!
    • b=Axb=Ax ist also Schreibweise für lineares GLS
    ⎛⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜⎝b0=A00x0+A01x1+…+A0,N−1xN−1b1=A10x0+A11x1+…+A1,N−1xN−1b2=A20x0+A21x1+…+A2,N−1xN−1⋮⋮⋮⋮bM−1=AM−1,0x0+AM−1,1x1+…+AM−1,N−1xN−1⎞⎟ ⎟ ⎟ ⎟ ⎟ ⎟ ⎟⎠(b0=A00x0+A01x1+…+A0,N−1xN−1b1=A10x0+A11x1+…+A1,N−1xN−1b2=A20x0+A21x1+…+A2,N−1xN−1⋮⋮⋮⋮bM−1=AM−1,0x0+AM−1,1x1+…+AM−1,N−1xN−1)
    • Implementierung
      • äußere Schleife über jj, innere für Summe
    • ACHTUNG: Init. b[j] = 0 nicht vergessen!
    In [ ]:
    1
    2
    3
    4
    5
    6
     for (j=0; j<M; j++) {
        b[j] = 0;
        for (k=0; k<N; k++) {
          b[j] = b[j] + A[j][k]*x[k];
        }
      }
    

    Matrix spaltenweise speichern 1/2¶

    • mathematische Bibliotheken speichern Matrizen idR. spaltenweise als Vektor

      • A∈RM×NA∈RM×N, gespeichert als a∈RMNa∈RMN
        • a=(A00,A10,...,AM−1,0,A01,A11,…,AM−1,N−1)a=(A00,A10,...,AM−1,0,A01,A11,…,AM−1,N−1)
    • muss Matrix spaltenweise speichern, wenn ich solche Bibliotheken nutzen will

      • diese Bibliotheken (aus historischen Gründen) meist in Fortran programmiert
    • Beachte: C indiziert j=0,...,M−1j=0,...,M−1, k=0,...,N−1k=0,...,N−1

      • AjkAjk ist (j+1)(j+1)-tes Element der (k+1)(k+1)-ten Spalte
      • also vorher kk Spalten mit k⋅Mk⋅M Speicherplätzen
      • plus jj Speicherplätze in der (k+1)(k+1)-ten Spalte
    • gezeigt: Ajk=aℓAjk=aℓ mit ℓ=j+k⋅Mℓ=j+k⋅M

    • BSP. BLAS (basic linear algebra subroutines)

      • alles, was man mit Vektoren machen kann...
      • elementare Operationen mit Matrizen...
    • BSP. LAPACK (linear algebra package)
      • lineare Gleichungssysteme lösen
      • Eigenwerte berechnen

    Matrix spaltenweise speichern 2/2¶

    • Erinnerung:

      • A∈RM×NA∈RM×N, gespeichert als a∈RMNa∈RMN
      • AjkAjk entspricht also aℓaℓ mit ℓ=j+k⋅Mℓ=j+k⋅M
    • Matrix-Vektor-Produkt

      • b:=Ax∈RMb:=Ax∈RM, bj=∑N−1k=0Ajkxkbj=∑k=0N−1Ajkxk
      • mit double A[M][N];
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // 2D-Array Speicherung
    for (j=0; j<M; j++) {
        b[j] = 0;
        for (k=0; k<N; k++) {
          b[j] = b[j] + A[j][k]*x[k];
        }
      }
    // 1D-Array spaltenweise Speicherung
    for (j=0; j<M; j++) {
        b[j] = 0;
        for (k=0; k<N; k++) {
          b[j] = b[j] + A[j+k*M]*x[k];
        }
      }
    

    MinSort (= Selection Sort)¶

    • Gegeben: Ein Vektor x∈Rnx∈Rn
    • Ziel: Sortiere xx, sodass x1≤x2≤⋯≤xnx1≤x2≤⋯≤xn

    • Algorithmus (1. Schritt)

      • suche Minimum xkxk von x1,…,xnx1,…,xn
      • vertausche x1x1 und xkxk, d.h. x1x1 ist kleinstes Elt.
    • Algorithmus (2. Schritt)
      • suche Minimum xkxk von x2,…,xnx2,…,xn
      • vertausche x2x2 und xkxk, d.h. x2x2 zweit kleinstes Elt.
    • nach n−1n−1 Schritten ist xx sortiert
    • Hinweise zur Realisierung (vgl. UE)
      • Länge nn ist Konstante im Hauptprogramm
      • i d.h. nn ist im Hauptprg nicht veränderbar
      • aber nn ist Inputparameter der Funktion \texttt{minsort}
      • i d.h. Funktion arbeitet für beliebige Länge

    MinSort¶

    • üblich: Kommentar, was eine Funktion tut, bei Vorwärts-Deklaration (Zeile 4--11)
    In [61]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    #include <stdio.h>
    #define DIM 5
    
    // read a double vector of length dim from the keyboard
    void scanVector(double input[], int dim);
    
    // print a double vector of length dim to the screen
    void printVector(double output[], int dim);
    
    // sort a double vector of length dim in ascending order
    void minsort(double vector[], int dim);
    
    int main() {
      double x[DIM];
      scanVector(x,DIM);
      minsort(x,DIM);
      printVector(x,DIM);
    }
    
    void scanVector(double input[], int dim) {
      int j = 0;
      for (j=0; j<dim; j++) {
        input[j] = 0;
        printf("%d: ",j);
        scanf("%lf",&input[j]);
      }
    }
    
    void printVector(double output[], int dim) {
      int j = 0;
      for (j=0; j<dim; j++) {
        printf("%f ",output[j]);
      }
      printf("\n");
    }
    
    void minsort(double vector[], int dim) {
      int j = 0;
      int k = 0;
      int argmin = 0;
      double tmp = 0;
      
      // iterate over all indices of the vector
      for (j=0; j<dim-1; j++) {
        // determine minimum in (vector[j],...,vector[dim-1])
        // initial guess: minimum is located at vector[j]
        argmin = j;
        for (k=j+1; k<dim; k++) {
          if (vector[argmin] > vector[k]) {
            argmin = k;
          }
        }
        // now vector[argmin] is minimum in the remainder
        // if needed, swap with j so that vector[j] is mininum
        if (argmin > j) {
          tmp = vector[argmin];
          vector[argmin] = vector[j];
          vector[j] = tmp;
        }
      }
    }
    
    0: 3
    1: 4
    2: 5
    3: 6
    4: 7
    3.000000 4.000000 5.000000 6.000000 7.000000
    

    break und continue¶

    • continue und break im statement von Schleifen

      • continue beendet aktuellen Durchlauf
      • break beendet nur die aktuelle Schleife
    • Code ist schlecht programmiertes Beispiel!

    In [62]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>
    
    int main() {
      int j = 0;
      int k = 0;
      
      for (j=0; j<4; ++j) {
        if (j%2 == 0) {
          continue;
        }
        for (k=0; k < 10; ++k) {
          printf("j=%d, k=%d\n",j,k);
          if (k > 1) {
            break;
          }
        }
      }
      printf("End: j=%d, k=%d\n",j,k);
    
    }
    
    j=1, k=0
    j=1, k=1
    j=1, k=2
    j=3, k=0
    j=3, k=1
    j=3, k=2
    End: j=4, k=2
    

    Aufwand von Algorithmen¶

    Aufwand eines Algorithmus¶

    • wichtige Kenngröße für Algorithmen
      • um Algorithmen zu bewerten / vergleichen
    • Aufwand = Anzahl benötigter Operationen
      • Zuweisungen
      • Vergleiche
      • arithmetische Operationen
    • programmspezifische Operationen nicht gezählt
      • Deklarationen & Initialisierungen
      • Schleifen, Verzweigungen etc.
      • Zählvariablen in Schleifen
    • Aufwand wird durch "einfaches" Zählen ermittelt
    • Konventionen zum Zählen nicht einheitlich
    • in EPROG ist Aufwand für worst case interessant
      • d.h. maximaler Aufwand im schlechtesten Fall
    • häufig ist auch "mittlerer Aufwand" interessant (d.h. Erwartungswert des Aufwands)
      • mathematisch komplizierter
      • deshalb nicht in EPROG
    • in EPROG (maximal) benötigte Formeln: n∑j=11=n,n∑j=1j=n(n+1)2,n∑j=1j2=n(n+1)(2n+1)6∑j=1n1=n,∑j=1nj=n(n+1)2,∑j=1nj2=n(n+1)(2n+1)6

    Beispiel: Maximum suchen¶

    • Beim Zählen wird jede Schleife zu einer Summe!

      • d.h. for in Zeile 6 ist ∑n−1i=1∑i=1n−1
    • Aufwand:

      • 11 Zuweisung⇝⇝ Zeile 5
      • In jedem Schritt der for-Schleife ⇝⇝ Zeile 6--10
        • 11 Vergleich ⇝⇝ Zeile 7
        • 11 Zuweisung (worst case!) ⇝⇝ Zeile 8
    • insgesamt Operationen 1+n−1∑i=12=1+2(n−1)=2n−11+∑i=1n−12=1+2(n−1)=2n−1

    • wobei Zählvariable nicht in Aufwand eingeht, d.h. nur Statement der for-Schleife wird berücksichtigt

    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    double maximum(double vector[], int n) {
      int i = 0;
      double max = 0;
      
      max = vector[0];
      for (i=1; i<n; i++) {
        if (vector[i] > max) {
          max = vector[i];
        }
      }
      
      return max;
    }
    

    Landau-Symbol OO (= groß-O)¶

    • oft nur Größenordnung des Aufwands interessant
    • Schreibweise f=O(g)f=O(g) für x→x0x→x0

      • heißt limsupx→x0∣∣∣f(x)g(x)∣∣∣<∞lim supx→x0|f(x)g(x)|<∞
      • d.h. es existiert eine Konstante C>0C>0 mit
      • i |f(x)|≤C|g(x)||f(x)|≤C|g(x)| für x→x0x→x0.

      • d.h. ff wächst höchstens so schnell wie gg für x→x0x→x0

    • Beispiel: Maximum suchen
      • Aufwand 2n−1=O(n)2n−1=O(n) für n→∞n→∞
    • häufig entfällt "für x→x0x→x0"
      • dann Grenzwert x0x0 kanonisch z.B. 2n−1=O(n)2n−1=O(n)
    • Sprechweise (nur Beispiele):
      • Algorithmus hat linearen Aufwand, falls Aufwand O(n)O(n) bei Problemgröße nn
        • Maximumssuche hat linearen Aufwand
      • Algorithmus hat fastlinearen Aufwand, falls Aufwand O(nlogn)O(nlog⁡n) bei Problemgröße nn
        • Die schnellsten Sortieralgorithmen (z.b. Quick-Sort, Merge-Sort) haben fast-linearen Aufwand)
      • Algorithmus hat quadratischen Aufwand, falls Aufwand O(n2)O(n2) bei Problemgröße nn
        • Matrix-Vektor Multiplikation hat quadratischen Aufwand
      • Algorithmus hat kubischen Aufwand, falls Aufwand O(n3)O(n3) bei Problemgröße nn
        • Gauss Elimination hat kubischen Aufwand
      • Algorithmus hat exponentiellen Aufwand falls Ausfwand O(an)O(an) für a>1a>1 bei Problemgröße nn
        • Primfaktorzerlegung mittels Sieb-von-Erastostenes einer nn-stelligen Zahl hat exponentiellen Aufwand. Die schnellsten Algorithmen (Zahlkörpersieb) sind nicht viel schneller.

    Matrix-Vektor Multiplikation¶

    • In jedem Schritt der jj-Schleife ⇝⇝ Zeile 6--11

      • 11 Zuweisung ⇝⇝ Zeile 7
      • In jedem Schritt der kk-Schleife ⇝⇝ Zeile 8--10
        • 22 Multiplikationen⇝⇝ Zeile 9
        • 22 Additionen⇝⇝ Zeile 9
        • 11 Zuweisung⇝⇝ Zeile 9
    • insgesamt Operationen m−1∑j=0(1+n−1∑k=05)=m+5mn∑j=0m−1(1+∑k=0n−15)=m+5mn

    • Aufwand O(mn)O(mn)
      • bzw. Aufwand O(n2)O(n2) für m=nm=n
      • d.h. quadratischer Aufwand für m=nm=n
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    void MVM(double A[], double x[], double b[],
             int m, int n) {
      int i = 0;
      int j = 0;
    
      for (j=0; j<m; j++) {
        b[j] = 0;
        for (k=0; k<n; k++) {
          b[j] = b[j] + A[j+k*m]*x[k];
        }
      }
    }
    

    Suchen im Vektor¶

    • Aufgabe:
      • Suche Index j mit vector[j] == value
      • Rückgabe -1, falls solcher nicht existiert
    • in jedem Schritt der jj-Schleife

      • 1 Vergleich
    • Anzahl der Operationen n−1∑j=01=n∑j=0n−11=n

    • Aufwand O(n)O(n) im worst case
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    int search(int vector[], int value, int n) {
    
      int j = 0;
      
      for (j=0; j<n; j++) {
        if (vector[j] == value) {
          return j;
        }
      }
    
      return -1;
    }
    

    Binäre Suche im sortierten Vektor¶

    • Voraussetzung: Vektor ist aufsteigend sortiert
    • Modifiziere Idee des Bisektionsverfahrens

      • d.h. natürliche Suche im Telefonbuch
      • Betrachte halben Vektor, falls vector[j]!= value
    • Frage: Wieviele Iterationen hat der Algorithmus?

      • jeder Schritt halbiert Vektor
      • Falls nn Zweierpotenz, gilt n/2k=1n/2k=1
      • dann maximal 1+log2n1+log2⁡n Schritte
      • i je 2 Vergl. + 2 Zuw. + 1 Mult. + 2 Add./Subtr.
    • Aufwand O(log2n)O(log2⁡n), d.h. logarithmischer Aufwand

      • sog. sublinearer Aufwand O(log2n)≪O(n)O(log2⁡n)≪O(n)
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int binsearch(int vector[], int value, int n) {
    
      int j = 0;
      int start = 0;
      int end = n-1;
    
      while (start <= end) {
        j = 0.5*(end + start);
        if (vector[j] == value) {
          return j;
        }
        else if (vector[j] > value) {
          end = j-1;
        }
        else {
          start = j+1;
        }
      }
    
      return -1;
    }
    

    Minsort¶

    • In jedem Schritt der jj-Schleife
      • 11 Zuweisung
      • In jedem Schritt der kk-Schleife
        • 11 Vergleich
        • 11 Zuweisung (worst case!)
      • jeweils 11 Vergleich
      • jeweils 33 Zuweisungen (worst case!)
    • quadratischer Aufwand O(n2)O(n2), weil: n−2∑j=0(5+n−1∑k=j+12)=5(n−1)+n−2∑j=0((n−(j+1))2=5(n−1)+2n−1∑k=1k=5(n−1)+2n(n−1)2∑j=0n−2(5+∑k=j+1n−12)=5(n−1)+∑j=0n−2((n−(j+1))2=5(n−1)+2∑k=1n−1k=5(n−1)+2n(n−1)2
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void minsort(int vector[], int n) {
      int j = 0;
      int k = 0;
      int argmin = 0;
      double tmp = 0;
    
      for (j=0; j<n-1; j++) {
        argmin = j;
        for (k=j+1; k<n; k++) {
          if (vector[argmin] > vector[k]) {
            argmin = k;
          }
        }
        if (argmin > j) {
          tmp = vector[argmin];
          vector[argmin] = vector[j];
          vector[j] = tmp;
        }
      }
    }
    

    Aufwand & Rechenzeit¶

    • Jede Operation kostet Rechenzeit

      • d.h. Aufwand korrespondiert zu Rechenzeit
    • Frage: Welche Rechenzeit kann ich erwarten?

      • Antwort kann nur eine relative Größe sein, da Rechner unterschiedlich schnell
      • theoretische Voraussagen
        • linearer Aufwand
          • Problemgröße nn →→ CnCn Operationen
          • Problemgröße knkn →→ CknCkn Operationen
          • d.h. 3×3× Problemgröße →→ 3×3× Rechenzeit
        • quadratischer Aufwand
          • Problemgröße nn →→ Cn2Cn2 Operationen
          • Problemgröße knkn →→ Ck2n2Ck2n2 Operationen
          • d.h. 3×3× Problemgröße →→ 9×9× Rechenzeit
        • etc.
    • BSP. Code braucht 11 Sekunde für n=1000n=1000

      • Aufwand O(n)O(n) →→ 1010 Sekunden für n=10000n=10000
      • Aufwand O(n2)O(n2) →→ 100100 Sekunden für n=10000n=10000
      • Aufwand O(n3)O(n3) →→ 10001000 Sek. für n=10000n=10000

    Zeitmessung¶

    • Wozu Zeitmessung?
      • Vergleich von Algorithmen / Implementierungen
      • Überprüfen theoretischer Voraussagen
    • Bibliothek time.h
      • Datentyp clock_t für Zeitvariablen für Ausgabe Typecast nicht vergessen!
      • Funktion clock() liefert Rechenzeit seit Programmbeginn
      • Konstante CLOCKS_PER_SEC zum Umrechnen: Zeitvariable/CLOCKS_PER_SEC liefert Angabe in Sekunden

    Beispiel: Zeitmessung¶

    • einbinden Codes von Folie 115--117 (Zeile 15--17)
    • dynamische Vektoren (Zeile 27--29) ⟶⟶ später!
      • ersetzt nur int vector[NMAX];
    • auch Test mit Zufallszahlen manchmal sinnvoll

      • hier auskommentiert (Zeile 38--44)
    • Bibliothek time.h

      • time(NULL) gibt Zeit seit 01.01.1970 in Sekunden
      • Fun fact: im Jahr 2038 reicht der Speicherplatz des Datentyps nicht mehr aus und es kommt zum Überlauf →→ UNIX 2028 Bug (siehe Zahldarstellung im Computer später)
    • Bibliothek stdlib.h

      • srand(time(NULL)) : Initialisierung des Zufallszahlen-Generators
        • RAND_MAX : maximale Zufallszahl (Konstante)
        • rand() liefert int Zufallszahl von 00 bis RAND_MAX
    • Vektor-Länge wächst =×2=×2

      • Zeit wächst ×2×2 linear bzw. ×4×4 quadratisch
    In [25]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    //%cflags:-I data/
    //ACHTUNG: Zeile 1 ist nur fuer die korrekte Funktion des interaktiven Skriptums noetig
    // include some libraries
    #include <stdio.h>
    #include <math.h>
    #include <time.h>
    
    // include library for dynamic storage allocation
    // see line 27-29 below
    #include <stdlib.h>
    
    // define minimal and maximal problem size
    #define NMIN (int)1e3
    #define NMAX (int)1e6
    
    // declaration of codes from previous slides
    #include "aufwand_search.c"
    #include "aufwand_binsearch.c"
    #include "aufwand_minsort.c"
    
    int main() {
      int n = NMIN;
      int j = 0;
      int tmp=0;
      clock_t start = 0;
      clock_t runtime = 0;
      clock_t previous_runtime = 0;
    
     // allocate dynamic vector ==> int vector[NMAX];
      printf("*** allocate integer vector, length=%d\n",NMAX);
      int* vector = malloc(NMAX*sizeof(int));
     
      // initialize vector with deterministic data
      printf("*** initialize vector\n");
      for (j=0; j<NMAX; ++j) {
        vector[j] = j;
      }
      
      /*
        // alternative: initialize vector with random numbers
        srand(time(NULL));
        // fill the vector with non-negative random numbers
        printf("*** generate random numbers\n");
        for (j=0; j<NMAX; ++j) {
          vector[j] = rand();
        }
      */
      
      // print the runtime for various vector lengths
      printf("*** determine computing times\n");
      for (n = NMIN; n <= NMAX; n = 2*n) {
        // save start time, call function, determine runtime
        start = clock();
        
         tmp = search(vector,-1,n); // linear complexity
        //tmp = binsearch(vector,-1,n); // logarithmic compl.
        //minsort(vector,n); // quadratic complexity
        
        runtime = clock() - start;
        
        // print runtime (and relative increase of runtime)
        if (n == NMIN) {
          printf("n=%d, runtime=%1.2f\n",n,
                 (double) runtime/CLOCKS_PER_SEC);
        }
        else {
          printf("n=%d, runtime=%1.2f, factor=%1.2f\n",
                 n, (double) runtime/CLOCKS_PER_SEC,
                 (double) runtime/previous_runtime);
        }
        previous_runtime = runtime;
      }
    }
    
    /tmp/tmpr2qorgxe.c: In function ‘main’:
    /tmp/tmpr2qorgxe.c:24:7: warning: variable ‘tmp’ set but not used [-Wunused-but-set-variable]
       24 |   int tmp=0;
          |       ^~~
    
    *** allocate integer vector, length=1000000
    *** initialize vector
    *** determine computing times
    n=1000, runtime=0.00
    n=2000, runtime=0.00, factor=1.20
    n=4000, runtime=0.00, factor=1.83
    n=8000, runtime=0.00, factor=1.91
    n=16000, runtime=0.00, factor=1.90
    n=32000, runtime=0.00, factor=2.10
    n=64000, runtime=0.00, factor=1.99
    n=128000, runtime=0.00, factor=2.02
    n=256000, runtime=0.00, factor=2.04
    n=512000, runtime=0.00, factor=2.00
    

    Vergleich von Laufzeit¶

    linear quadratisch fastlinear
    nn search minsort binsearch
    1.000 0.00 0.00 0.00
    2.000 0.00 0.00 0.00
    4.000 0.00 0.01 0.00
    8.000 0.00 0.06 0.00
    16.000 0.00 0.25 0.00
    32.000 0.00 1.03 0.00
    64.000 0.00 4.12 0.00
    128.000 0.00 16.55 0.00
    256.000 0.00 64.31 0.00
    512.000 0.00 257.25 0.00
    1.024.000 0.00 >18min 0.00
    2.048.000 0.01 >72min 0.00
    4.096.000 0.01 >4,5h 0.00
    8.192.000 0.02 >18h 0.00
    16.384.000 0.04 >3d 0.00
    32.768.000 0.08 >12d 0.00
    65.536.000 0.15 >1,5m 0.00
    131.072.000 0.29 >6m 0.00
    262.144.000 0.60 >2y 0.00
    524.288.000 1.18 >8y 0.00
    1.048.576.000 2.53 >32y 0.00

    Aufwand: Zusammenfassung¶

    • Quadratischer Aufwand für große nn spürbar
    • Fazit: Algorithmen sollen kleinen Aufwand haben
      • Ziel der numerischen Mathematik
      • nicht immer möglich, oft offene Frage, z.B., Aufwand für Lösen von LGS, oder P≠NPP≠NP in der theoretischen Informatik

    Pointer¶

    Variablen¶

    • Variable = symbolischer Name für Speicherbereich

      • und Information, wie Speicherbereich interpretiert werden muss (Datentyp laut Deklaration)
    • Compiler übersetzt Namen in Referenz auf Speicherbereich und merkt sich, wie dieser interpretiert werden muss

    Pointer¶

    • Pointer = Variable, die Adresse eines Speicherbereichs enthält
    • Dereferenzieren = Zugriff auf den Inhalt eines Speicherbereichs mittels Pointer
      • d.h. Schreiben eines Wertes an diese Adresse bzw. Auslesen des dort gespeicherten Wertes
        • Beim Dereferenzieren muss Compiler wissen, welcher Var.typ im gegebenen Speicherbereich liegt, d.h. wie Speicherbereich interpretiert werden muss

    Pointer in C¶

    • Pointer & Variablen sind in C eng verknüpft:

      • var Variable →→ &var ist zugehöriger Pointer
      • ptr Pointer →→ *ptr ist zugehörige Variable
      • insbesondere *&var = var sowie &*ptr = ptr
    • Bei Deklaration muss Typ des Pointers angegeben werden, da *ptr eine Variable sein soll!

      • int* ptr; deklariert ptr als Pointer auf int
      • d.h. ptr speichert eine Adresse, an welcher ein int im Speicher abgelegt ist
    • Wie üblich gleichzeitige Initialisierung möglich

      • int var; deklariert Variable var vom Typ int
      • int* ptr = &var; deklariert ptr und weist Speicheradresse der Variable var zu
        • Bei solchen Zuweisungen muss der Typ von Pointer und Variable passen, sonst passiert Unglück (Laufzeitfehler)!
        • I.a. gibt Compiler eine Warnung aus, z.B. incompatible pointer type
        • Diese Warnung nie ignorieren, sondern Code checken, da 99\% Garantie auf Laufzeitfehler!
    • Analog für andere Datentypen, z.B. double

    Ein elementares Beispiel¶

    • %p Platzhalter für printf für Adresse
      • manchmal sinnvoll zum Finden von Fehlern, dass man Werte von Variablen ausgibt!
    In [69]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>
    
    int main() {
      int var = 1;
      int* ptr = &var;
    
      printf("a) var = %d, *ptr = %d\n",var,*ptr);
    
      var = 2;
      printf("b) var = %d, *ptr = %d\n",var,*ptr);
    
      *ptr = 3;
      printf("c) var = %d, *ptr = %d\n",var,*ptr);
    
      var = 47;
      printf("d) *(&var) = %d,",*(&var));
      printf("*&var = %d\n",*&var);
        
      printf("e) &var = %p\n", &var);
    }
    
    a) var = 1, *ptr = 1
    b) var = 2, *ptr = 2
    c) var = 3, *ptr = 3
    d) *(&var) = 47,*&var = 47
    e) &var = 0x7fffc438a6ec
    

    Begrifflichkeiten¶

    • Call by Value

      • Funktionen erhalten Werte der Input-Parameter und speichern diese in lokalen Variablen
      • Änderungen an den Input-Parameter wirken sich nicht außerhalb der Funktion aus
    • Call by Reference

      • Funktionen erhalten Variablen als Input ggf. unter lokal neuem Namen
      • Änderungen an den Input-Parametern wirken sich außerhalb der Funktion aus

    Call by Value / Reference in C¶

    • In C gibt es nur Call by Value
    • Kann Call by Reference mittels Pointern realisieren
    • Vektoren werden mit Call by Reference übergeben
      • kein Widerspruch, Erklärung folgt gleich!

    Call by Reference in C¶

    • Elementare Datentypen werden in C mit Call by Value an Funktionen übergeben
      • z.B. int, double, Pointer
    • Call by Reference ist über Pointer realisierbar:

    • Die Funktion test ändert den Wert der Variable x, da mittels Pointer y direkt auf den Speicher zugegriffen wird.

    In [70]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #include <stdio.h>
    
    void test(int* y) {
      printf("a) *y=%d\n", *y);
      *y = 43;
      printf("b) *y=%d\n", *y);
    }
    
    int main() {
      int x = 12;
      printf("c) x=%d\n", x);
      test(&x);
      printf("d) x=%d\n", x);
    }
    
    c) x=12
    a) *y=12
    b) *y=43
    d) x=43
    

    Beispiel¶

    • determineMinMax soll Minimum und Maximum eines Vektors finden
      • d.h. zwei Rückgabe-Parameter
      • in C maximal ein Rückgabe-Parameter erlaubt
        • Verwende Pointer für Call by Reference
    • mehr als eine Funktionalität = schlechter Stil:
      • jede Funktion hat genau eine Funktionalität
      • jede Funktionalität erfordert eine Funktion
    • oft trotzdem verwendet, weil effizienter:
      • hier nur eine Schleife, statt zwei!
    In [71]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    #include <stdio.h>
    #include <assert.h>
    #define DIM 5
    
    // The function scanVector reads the coefficients
    // of a double vector of length dim from the keyboard
    void scanVector(double input[], int dim);
    
    // The function determineMinMax returns the minimum
    // and maximum of a double vector of length dim. We
    // use Call-by-Reference because of 2 return values.
    
    void determineMinMax(double vector[],int dim,
                         double* min, double* max);
    
    int main() {
      double x[DIM];
      double max = 0;
      double min = 0;
      scanVector(x,DIM);
      determineMinMax(x,DIM, &min, &max);
      printf("min(x) = %f\n",min);
      printf("max(x) = %f\n",max);
    }
    
    void scanVector(double input[], int dim) {
      assert(dim > 0);
      int j = 0;
      for (j=0; j<dim; ++j) {
        input[j] = 0;
        printf("%d: ",j);
        scanf("%lf",&input[j]);
      }
    }
    
    void determineMinMax(double vector[],int dim,
                         double* min, double* max) {
      int j = 0;
      assert(dim > 0);
    
      // First guess for minimum as well as maximum is the
      // first vector coefficient.
      // Recall that, e.g., max is double* so that *max is
      // a double variable.
      *max = vector[0];
      *min = vector[0];
      
      // iterate through vector and search for smaller resp.
      // larger elements to determine minimum and maximum.
      for (j=1; j<dim; ++j) {
        if (vector[j] < *min) {
          *min = vector[j];
        }
        else if (vector[j] > *max) {
          *max = vector[j];
        }
      }
    }
    
    0: 4
    1: 3
    2: 5
    3: 6
    4: 7
    min(x) = 3.000000
    max(x) = 7.000000
    

    Anmerkungen zu Pointern¶

    • Sind int*, double* Datentypen?

    • CC-Erfinder sagen Nein!

      • int *pointer deklariert Pointer auf int
      • im Gegensatz zu: pointer ist vom Datentyp int*
    • Verschiedene Sichtweisen sind beinahe äquivalent

    • Leerzeichen wird vom Compiler ignoriert:

      • int* pointer, int * pointer, int *pointer
    • * wird nur auf den folgenden Namen bezogen

    • ACHTUNG bei Deklaration von Listen:

      • int* pointer, var; deklariert Pointer auf int und Variable vom Typ int
      • int *pointer1, *pointer2; deklariert zwei Pointer auf int
    • ALSO Listen von Pointern vermeiden!

      • auch zwecks Lesbarkeit!

    Elementare Datentypen¶

    Elementare Datentypen¶

    • Datentyp für Zeichen (z.B. Buchstaben)

      • char
    • Datentypen für Ganzzahlen:

      • int
    • Datentypen für Gleitkommazahlen:

      • float (einfach genaue Gleitk.zahlen, Single)
      • double (doppelt genaue Gleitkommazahlen)
    • Alle Pointer gelten als elementare Datentypen

    Qualifier (nicht in EPROG!)}

    Qualifier (nicht in EPROG)¶

    • signed und unsigned für char und int
    • long und short für int
    • long für double
      • z.B. Datentyp signed char, unsigned long int
      • siehe https://en.wikipedia.org/wiki/C_data_types

    Bemerkungen¶

    • Deklaration und Gebrauch wie bisher

    • Man kann Arrays & Pointer bilden

    • Für UE: nur char, int, double Pointer

    • Genaueres zu den Typen später!

    Funktionen¶

    • Elementare Datentypen werden an Funktionen mit Call by Value übergeben

    • Return Value darf nur void oder elementar sein

    Arrays¶

    • Streng genommen, gibt es in C keine Arrays!

      • Deklaration int array[N];
        • legt Pointer array vom Typ int* an
        • organisiert ab der Adresse array Speicher, um N-mal einen int zu speichern
        • d.h. array enthält Adresse von array [0]
        • d.h. array == & array[0]
        • d.h. *array == array[0]
        • Da Pointer als elementare Datentypen mittels Call by Value übergeben werden, werden Arrays augenscheinlich mit Call by Reference übergeben
    • Pointerarithmetik: array+j == & array [j]

      • d.h. *(array + j) == array [j]

    Laufzeitfehler!¶

    • Ziel: scanVector soll Vektor anlegen & initialisieren
    • Syntax des Programms ist OK, aber Laufzeitfehler!
    • Problem: Speicher zu x mit Blockende 14 aufgelöst
      • d.h. Pointer aus 6/13 zeigt auf Irgendwas
    • Abhilfe: Call by Reference (vorher!) oder händische Speicherverwaltung (gleich!)
    In [72]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #include <stdio.h>
    #include <assert.h>
    
    double* scanVector(int length) {
      assert(length > 0);
      double vector[length];
      int j = 0;
      for (j=0; j<length; ++j) {
        vector[j] = 0;
        printf("vector[%d] = ",j);
        scanf("%lf",&vector[j]);
      }
      return vector;
    }
    
    int main() {
      double* x;
      int j = 0;
      int dim = 0;
      
      printf("dim = ");
      scanf("%d",&dim);
      
      x = scanVector(dim);
    
      for (j=0; j<dim; ++j) {
        printf("x[%d] = %f\n",j,x[j]);
      }
    }
    
    /tmp/tmpw5npq09t.c: In function ‘scanVector’:
    /tmp/tmpw5npq09t.c:13:10: warning: function returns address of local variable [-Wreturn-local-addr]
       13 |   return vector;
          |          ^~~~~~
    
    dim = 4
    vector[0] = 3
    vector[1] = 4
    vector[2] = 5
    vector[3] = 6
    
    [C kernel] Executable exited with code -11

    Der Befehl sizeof¶

    • Ist var eine Variable eines elementaren Datentyps, gibt sizeof(var) die Größe der Var. in Bytes zurück
    • Ist type ein Datentyp, so gibt sizeof(type) die Größe einer Variable dieses Typs in Bytes zurück
    • Ist array lokales statisches Array im selben Block, so liefert sizeof(array) Größe des Arrays in Bytes
    • Intern sind ptr und array zwei double Pointer und enthalten (= zeigen auf) dieselbe Speicheradresse!
    In [73]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    
    void printSizeOf(double vector[]) {
      printf("sizeof(vector) = %d\n",sizeof(vector));  
    }
    
    int main() {
      int var = 43;
      double array[12];
      double* ptr = array;
    
      printf("sizeof(var) = %d\n",sizeof(var));
      printf("sizeof(double) = %d\n",sizeof(double));
      printf("sizeof(array) = %d\n",sizeof(array));
      printf("sizeof(ptr) = %d\n",sizeof(ptr));
      printSizeOf(array);
    }
    
    /tmp/tmp644_iya1.c: In function ‘printSizeOf’:
    /tmp/tmp644_iya1.c:4:40: warning: ‘sizeof’ on array function parameter ‘vector’ will return size of ‘double *’ [-Wsizeof-array-argument]
        4 |   printf("sizeof(vector) = %d\n",sizeof(vector));
          |                                        ^
    /tmp/tmp644_iya1.c:3:25: note: declared here
        3 | void printSizeOf(double vector[]) {
          |                  ~~~~~~~^~~~~~~~
    
    sizeof(var) = 4
    sizeof(double) = 8
    sizeof(array) = 96
    sizeof(ptr) = 8
    sizeof(vector) = 8
    

    Dynamische Speicherverwaltung¶

    Statische Arrays¶

    • double array[N]; deklariert statischen Vektor array der Länge N mit double-Komponenten

      • Indizierung array[j] mit 0≤j≤N−10≤j≤N−1
      • array ist intern vom Typ double*
        • enthält Adr. von array[0], sog. Base Pointer
      • Länge N kann während Programmablauf nicht verändert werden
    • Speicher des Arrays wird mit Blockende aufgelöst, d.h., kann keine statischen Arrays in Funktionen anlegen und zurückgeben (siehe letztes Beispiel)

    Speicher allokieren¶

    • Jetzt: händische Speicherverwaltung von Arrays

      • dadurch Vektoren dynamischer Länge möglich
      • Speicher wird erst durch expliziten Befehl aufgelöst
    • Einbinden der Standard-Bibl: #include <stdlib.h>

      • 3 neue Befehle: malloc, free, realloc
      • neue Konstante: NULL
    • pointer = malloc(N*sizeof(type));

      • allokiert Speicher für Vektor der Länge N mit Komponenten vom Typ type
        • malloc kriegt Angabe in Bytes →→ sizeof
      • pointer muss vom Typ type* sein
        • Base Pointer pointer bekommt Adresse der ersten Komponente pointer[0]
      • pointer und N muss sich Programm merken!
    • Häufiger Laufzeitfehler: sizeof vergessen!

    • Achtung: Allokierter Speicher ist uninitialisiert!

    • Konvention: Pointer ohne Speicher bekommen den Wert NULL zugewiesen

      • führt sofort auf Speicherzugriffsfehler bei Zugriff
    • malloc liefert NULL, falls Fehler bei Allokation

      • d.h. Speicher konnte nicht allokiert werden

    Speicher freigeben¶

    • free(pointer)

      • gibt Speicher eines dyn. Vektors frei
      • pointer muss Output von malloc sein
    • Achtung: Speicher wird freigegeben, aber pointer existiert weiter

      • Erneuter Zugriff führt (irgendwann) auf Laufzeitfehler
    • Achtung: Speicher freigeben, nicht vergessen!

      • und Pointer auf NULL setzen!

    Beispiel¶

    • Ziel: scanVector soll Vektor anlegen & einlesen

      • Vergleiche Beispiel mit statischen Vektoren
    • Jetzt aber Code OK, da Funktion den Vektor dynamisch mit malloc allokiert

      • Vektor existiert (d.h. Speicher ist reserviert), bis Basepointer mit free freigegeben!
    In [74]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    
    // scanVector allocates a double vector of prescribed
    // length and reads the coefficients from the keyboard
    double* scanVector(int length) {
      int j = 0;
      double* vector = NULL;
      // ensure that given length is admissible
      assert(length > 0);
     
      // allocate memory and ensure that this was successful
      vector = malloc(length*sizeof(double));
      assert(vector != NULL);
      
      // read coefficients from keyboard
      for (j=0; j<length; ++j) {
        vector[j] = 0;
        printf("vector[%d] = ",j);
        scanf("%lf",&vector[j]);
      }
      // return base pointer of dynamic vector
      return vector;
    }
    
    // printVector prints vector of given length in the shell
    void printVector(double* vector, int length) {
      int j = 0;
      // ensure that vector and length are admissible
      assert(vector != NULL);
      assert(length > 0);
    
      // print coefficients to shell
      for (j=0; j<length; ++j) {
        printf("vector[%d] = %f\n",j,vector[j]);
      }
    }
    
    int main() {
      // always set pointers to NULL, if not bound to memory
      double* x = NULL;
      int dim = 0;
    
      // read vector length from keyboard
      printf("dim = ");
      scanf("%d",&dim);
      
      // allocate vector, read coefficients, and print vector
      x = scanVector(dim);
      printVector(x,dim);
    
      // do not forget to free memory
      free(x);
      // set pointer x to NULL, since not bound to memory
      x = NULL;
    }
    
    dim = 4
    vector[0] = 1
    vector[1] = 2
    vector[2] = 3
    vector[3] = 4
    vector[0] = 1.000000
    vector[1] = 2.000000
    vector[2] = 3.000000
    vector[3] = 4.000000
    

    Dynamische Vektoren¶

    • pointer = realloc(pointer, Nnew*sizeof(type))
      • verändert Speicherallokation
        • zusätzliche Allokation für Nnew > N
        • Speicherbereich kürzen für Nnew < N
      • Alter Inhalt bleibt (soweit möglich) erhalten
      • Rückgabe NULL bei Fehler
    In [105]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    
    int main() {
      int N = 5;
      int Nnew = 10;
      int j = 0;
      
      int* array = malloc(N*sizeof(int));
      assert(array != NULL);
      for (j=0; j<N; ++j){
        array[j] = j;
      }
      
      array = realloc(array,Nnew*sizeof(int));
      assert(array != NULL);
      for (j=N; j<Nnew; ++j){
        array[j] = 10*j;
      }
    
      for (j=0; j<Nnew; ++j){
        printf("%d ",array[j]);
      }
      printf("\n");
      free(array);
      array = NULL;
    }
    
    0 1 2 3 4 50 60 70 80 90
    

    Bemerkungen¶

    • Base Pointer (= Output von malloc bzw. realloc) merken & nicht verändern
      • notwendig für fehlerfreies free und realloc
    • bei malloc und realloc nicht sizeof vergessen
      • Typ des Base Pointers muss zum sizeof passen!
    • Output von malloc und realloc auf NULL checken
      • sicherstellen, dass Speicher allokiert wurde!
    • allokierter Speicherbereich ist stets uninitialisiert
      • nach Allokation stets initialisieren
    • Länge des dynamischen Arrays merken
      • kann Programm nicht herausfinden!
    • Nicht mehr benötigten Speicher freigeben
      • insb. vor Blockende ...}, da dann Base Pointer weg
    • Pointer auf NULL setzen, wenn ohne Speicher
      • Fehlermeldung, falls Programm "aus Versehen" auf Komponente array[j] zugreift
    • Nie realloc, free auf statisches Array anwenden
      • Führt auf Laufzeitfehler, da Compiler free selbständig hinzugefügt hat!
    • Ansonsten gleicher Zugriff auf Komponenten wie bei statischen Arrays
      • Indizierung array[j] für 0≤j≤N−10≤j≤N−1

    Anwendung für Pointer: Eine Vektor Bibliothek¶

    Aufteilen von Source-Code¶

    • längere Source-Codes auf mehrere Files aufteilen

    • Vorteil:

      • übersichtlicher
      • Bildung von Bibliotheken
        • Wiederverwendung von alten Codes
        • vermeidet Fehler
    • gcc name1.c name2.c ...

      • erstellt ein Executable aus Source-Codes
      • Reihenfolge der Codes nicht wichtig
      • analog zu gcc all .c
        • wenn all .c ganzen Source-Code enthält
      • insb. Funktionsnamen müssen eindeutig sein
      • main() darf nur 1x vorkommen

    Preprocessor, Compiler & Linker¶

    • Beim Kompilieren von Source-Code werden mehrere Stufen durchlaufen:

      • Preprocessor-Befehle ausführen, z.B. #include
      • Compiler erstellt Objekt-Code
      • Objekt-Code aus Bibliotheken wird hinzugefügt
      • Linker ersetzt symbolische Namen im Objekt-Code durch Adressen und erzeugt Executable
    • Bibliotheken = vorkompilierter Objekt-Code

      • plus zugehöriges Header-File
    • Standard-Linker in Unix ist ld

    • Nur Schritt (3) fertig, d.h. Objekt-Code erzeugen

      • gcc -c name .c erzeugt Objekt-Code name .o
      • gut zum Debuggen von Syntax-Fehlern!
    • Objekt-Code händisch hinzufügen + kompilieren

      • gcc name.c bib1.o bib2.o ...
      • gcc name.o bib1.o bib2.o ...
      • Reihenfolge + Anzahl der Objekt-Codes ist egal
    • Ziel: selbst Bibliotheken erstellen
      • spart ggf. Zeit beim Kompilieren
      • vermeidet Fehler

    Eine erste Bibliothek¶

    • Header-File dynamicvectors.h zur Bibliothek
      • enthält alle Funktionssignaturen
      • enthält Kommentare zu den Funktionen
    • sog. Include Guard: Header-File beginnt mit #ifndef NAME #define NAME
    • Header-File ended mit #endif

    • keine erneute Deklaration bei mehrfachen #include

    In [76]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #ifndef INCLUDE_DYNAMICVECTORS__
    #define INCLUDE_DYNAMICVECTORS__
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    
    // allocate + initialize dynamic double vector of length n
    double* mallocVector(int n);
    
    // free a dynamic vector and set the pointer to NULL
    double* freeVector(double* vector);
    
    // extend dynamic double vector and initialize new entries
    double* reallocVector(double* vector, int n, int nnew);
    
    // allocate dynamic double vector of length n and read
    // entries from keyboard
    double* scanVector(int n);
    
    // print dynamic double vector of length n to shell
    void printVector(double* vector, int n);
    
    #endif
    

    Source-Code¶

    • Einbinden des Header-Files (Zeile 1)
      • #include "..." mit Angabe des Verzeichnis
      • #include <...> für Standard-Verzeichnis
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    #include "dynamicvectors.h"
    
    double* mallocVector(int n) {
      int j = 0;
      double* vector = NULL;
      assert(n > 0);
    
      vector = malloc(n*sizeof(double));
      assert(vector != NULL);
     
      for (j=0; j<n; ++j) {
        vector[j] = 0;
      }
      return vector;
    }
    
    double* freeVector(double* vector) {
      free(vector);
      return NULL;
    }
    
    double* reallocVector(double* vector, int n, int nnew) {
      int j = 0;
      assert(vector != NULL);
      assert(n > 0);
      assert(nnew > 0);
      
      vector = realloc(vector,nnew*sizeof(double));
      assert(vector != NULL);
      for (j=n; j<nnew; ++j) {
        vector[j] = 0;
      }
      return vector;
    }
    
    double* scanVector(int n) {
      int j = 0;
      double* vector = NULL;
      assert(n > 0);
    
      vector = mallocVector(n);
      assert(vector != NULL);
      
      for (j=0; j<n; ++j) {
        printf("vector[%d] = ",j);
        scanf("%lf",&vector[j]);
      }
      return vector;
    }
    
    void printVector(double* vector, int n) {
      int j = 0;
      assert(vector != NULL);
      assert(n > 0);
    
      for (j=0; j<n; ++j) {
        printf("%d: %f\n",j,vector[j]);
      }
    }
    

    Hauptprogramm¶

    • Hauptprogramm bindet Header der Bibliothek ein
    • Kompilieren mittels
      • gcc -c dynamicvectors.c
        • erzeugt Object-Code dynamicvectors.o
      • gcc dynamicvectors_main .c dynamicvectors.o
        • erzeugt Executable a.out
    In [23]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //%cflags:data/dynamicvectors.o -I data/
    //ACHTUNG: Zeile 1 ist nur fuer die korrekte Funktion des interaktiven 
    //         Skriptums noetig (ersetzt gcc ... dynamicvectors.o)
    #include "dynamicvectors.h"
    
    int main() {
      double* x = NULL;
      int n = 10;
      int j = 0;
      
      x = mallocVector(n);
      for (j=0; j<n; ++j) {
        x[j] = j;
      }
      
      x = reallocVector(x,n,2*n);
      for (j=n; j<2*n; ++j) {
        x[j] = 10*j;
      }
      
      printVector(x,2*n);
      x = freeVector(x);
    }
    
    0: 0.000000
    1: 1.000000
    2: 2.000000
    3: 3.000000
    4: 4.000000
    5: 5.000000
    6: 6.000000
    7: 7.000000
    8: 8.000000
    9: 9.000000
    10: 100.000000
    11: 110.000000
    12: 120.000000
    13: 130.000000
    14: 140.000000
    15: 150.000000
    16: 160.000000
    17: 170.000000
    18: 180.000000
    19: 190.000000
    

    Strings¶

    Strings (= Zeichenketten)¶

    • Strings = char-Arrays, also 2 Definitionen möglich
      • statisch: char array[N];
        • N = statische Länge
        • Deklaration & Initialisierung möglich char array[] = "text";
      • dynamisch (wie oben, Typ: char*)
    • Fixe Strings in Anführungszeichen "..."
    • Einzelnes Zeichen in Hochkommata '.'
    • Zugriff auf Teil-Strings nicht möglich!
    • Achtung bei dynamischen Strings:

      • als Standard enden alle Strings mit Null-Byte \0
        • kann Länge eines Strings dadurch bestimmen!
      • Bei statischen Arrays geschieht das automatisch (also wirkliche Länge N+1 und array[N] ='\0')
      • Bei dyn. Strings also 1 Byte mehr reservieren!
      • und \0 nicht vergessen
    • An Funktionen können auch fixe Strings (in Anführungszeichen) übergeben werden

      • z.B. printf("Hello World!\n");

    Funktionen zur String-Manipulation¶

    • Wichtigste Funktionen in stdio.h

      • sprintf / sscanf agieren wie printf / scanf, aber Output / Input = String
    • zahreiche Funktionen in stdlib.h, z.B.

      • atof: konvertiert String →→ double
      • atoi: konvertiert String →→ int
    • oder in string.h, z.B.

      • strchr, memchr: Suche char innerhalb String
      • strcmp, memcmp: Vergleiche zwei Strings
      • strcpy, memcpy: Kopieren von Strings
      • strlen: Länge eines Strings (ohne Null-Byte)
    • Header-Files mit #include <name> einbinden!

    • Gute Referenz mit allen Befehlen & Erklärungen

      • https://en.wikibooks.org/wiki/C_Programming
      • z.B. https://en.wikibooks.org/wiki/C_Programming/string.h/Function_reference
    • Details zu den Befehlen in UNIX mit man 3 befehl

      • sog. UNIX programmer's manual
      • z.B. Eingabe man 3 strcpy in der Shell
    • ACHTUNG mit String-Befehlen: Befehle können nicht wissen, ob für den Output-String genügend Speicher allokiert ist (→→ Laufzeitfehler!)

    Beispiel¶

    • Fixe Strings in Anführungszeichen "..."(Zeile 21)

      • erzeugt statisches Array mit zusätzlichem Null-Byte am Ende
    • Zugriff auf einzelnes Zeichen eines Strings mit einfachen Hochkommata '.' (Zeile 23)

    • Platzhalter für Strings in printf ist %s (Zeile 24)

    In [79]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <assert.h>
    
    char* stringCopy(char* source) {
      int length = 0;
      char* result = NULL;
      // ensure that source string exists
      assert(source != NULL);
      
      // determine length and allocate memory
      length = strlen(source);
      result = malloc((length+1)*sizeof(char));
      // copy source string and return copy
      strcpy(result,source);
      return result;
    }
    
    int main() {
      char* string1 = "Hello World?";
      char* string2 = stringCopy(string1);
      string2[11] = '!';
      printf("%s %s\n",string1,string2);
      free(string2);
      string2=NULL;
    }
    
    Hello World? Hello World!
    

    Funktionspointer¶

    Funktionspointer¶

    • Funktionsaufruf ist Sprung an eine Adresse

      • Pointer speichern Adressen
      • kann daher Fkt-Aufruf mit Pointer realisieren
    • Deklaration eines Funktionspointers:

      • <return value>(*pointer)(<input>); deklariert Pointer pointer für Funktionen mit Parametern <input> und Ergebnis vom Typ <return value>
      • Notation entpricht der C-Denkweise:
        • Was bekommt man, wenn man pointer dereferenziert? Funktion fct mit Signatur <return value>fct(<input>)
    • Bei Zuweisung müssen Pointer pointer und Funktion denselben Aufbau haben

      • gleicher Return-Value
      • gleiche Input-Parameter-Liste
    • Aufruf einer Funktion über Pointer wie bei normalem Funktionsaufruf!

    Elementares Beispiel¶

    • Deklaration eines Funktionspointers in Zeile 13
    • Zuweisung auf Fkt.pointer in Zeile 16 + 20
    • Fkt.aufruf über Pointer in Zeile 17 + 21
    • Achtung: kein dereferenzieren notwendig (*output)(string) ist gleich output(string)
    In [80]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <stdio.h>
    
    void output1(char* string) {
      printf("*%s*\n",string);
    }
    
    void output2(char* string) {
      printf("#%s#\n",string);
    }
    
    int main() {
      char string[] = "Hello World";
      void (*output)(char* string) = NULL;
    
      // call function output1 via function pointer
      output = output1;
      output(string);
     
      // call function output2 via function pointer
      output = output2;
      output(string);
    }
    
    *Hello World*
    #Hello World#
    

    Beispiel: Bisektionsverfahren (schon wieder)¶

    • Approximation der Nullstelle von f(x)=x2+ex−2f(x)=x2+ex−2 auf dem Intervall [0,10][0,10] auf Genauigkeit 10−1210−12
      • f(0)=−1<0<98+e10=f(10)f(0)=−1<0<98+e10=f(10)
    In [81]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    #include <stdio.h>
    #include <assert.h>
    #include <math.h>
    
    // bisection returns an approximate root x0 of a
    // continuous real-valued scalar function fct with
    // fct(a) * fct(b) <= 0, for which the intermediate
    // value theorem yields the existence of a root.
    //
    // bisection takes the function pointer of the function
    // fct: [a,b] -> R and a tolerance tol and returns x0
    // in (a,b) such that |x0 - x| <= tol, where fct(x) = 0.
    
    double bisection(double (*fct)(double x),
                     double a, double b, double tol);
    
    // demo function f
    double f(double x) {
      return x*x + exp(x) - 2;
    }
    
    int main() {
      double a = 0;
      double b = 10;
      double tol = 1e-12;
       
      double x = bisection(f,a,b,tol);
      printf("Approximate zero of the function x=%1.15e\n",x);
    }
    
    double bisection(double (*fct)(double x),
                     double a, double b, double tol) {
      double m = 0;
      double fa = 0;
      double fm = 0;
      
      // ensure that input is admissible
      assert(a < b);
      fa = fct(a);
      assert(fa*fct(b) <= 0);
      
      // iterate by interval bisection
      while ( b-a > 2*tol) {
        // compute interval mitpoint and evaluate fct
        m = (a+b)/2;
        fm = fct(m);
        if ( fa*fm <= 0 ) {
          // theorem guarantees root in [a,m]
          b = m;
        }
        else {
          // theorem guarantees root in [m,b]
          a = m;
          fa = fm;
        }
      }
      // as loop terminates, x in [a,b] and |b-a| <= 2*tol
      // so that |x-m| <= tol for the midpoint m.
      return m;
    }
    
    Approximate zero of the function x=5.372744491739923e-01
    

    Strukturen¶

    Deklaration von Strukturen¶

    • Funktionen
      • Zusammenfassung von verschiedenen Befehlen, um Abstraktionsebenen zu schaffen
    • Strukturen

      • Zusammenfassung von Variablen verschiedener Datentypen zu einem neuen Datentyp
      • Abstraktionsebenen bei Daten
    • Beispiel: Verwaltung der EPROG-Teilnehmer

      • pro Student jeweils denselben Datensatz
    • Semikolon nach Struktur-Deklarations-Block

    • erzeugt neuen Variablen-Typ Student
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // declaration of structure and corresponding data type
    typedef struct _Student_ {
      char* firstname;
      char* lastname;
      int studentID;
      int studiesID;
      double final_test;
      double kreuzerl;
      double exercise;
    } Student;
    

    Strukturen & Members¶

    • Datentypen einer Struktur heißen Members
    • Zugriff auf Members mit Punkt-Operator
      • var Variable vom Typ Student
      • z.B. Member var.firstname
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // declaration of structure
    struct _Student_ {
      char* firstname;   // first name
      char* lastname;    // last name
      int studentID;     // student ID
      int studiesID;     // studies ID
      double final_test; // points in final test
      double kreuzerl; // percentage of Kreuzerl
      double exercise;   // points in exercises
    };
    
    // declaration of corresponding data type
    typedef struct _Student_ Student;
    
    main() {
      // declaration of variable var of type Student
      Student var;
      // assign values to members of struct variable
      var.firstname = "Dirk";
      var.lastname = "Praetorius";
      var.studentID = 0;
      var.studiesID = 680;
      var.final_test = 25.;
      var.kreuzerl = 0.7;
      var.exercise = 35.;
    }
    

    Kombinierte Deklaration¶

    • gleichzeitige Deklaration von struct und typedef
    • anstelle von struct _Student_ { ... }; typedef struct _Student_ Student;
    • einfach typedef struct _Student_{ ... } Student;
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // declaration of structure and corresponding data type
    typedef struct _Student_ {
      char* firstname;
      char* lastname;
      int studentID;
      int studiesID;
      double final_test;
      double kreuzerl;
      double exercise;
    } Student;
    
    main() {
      // declaration of variable var of type Student
      Student var;
      // assign values to members of struct variable
      var.firstname = "Dirk";
      var.lastname = "Praetorius";
      var.studentID = 0;
      var.studiesID = 680;
      var.final_test = 25.;
      var.kreuzerl = 0.7;
      var.exercise = 35.;
    }
    

    Bemerkungen zu Strukturen¶

    • laut erstem C-Standard verboten:

      • Struktur als Input-Parameter einer Funktion
      • Struktur als Output-Parameter einer Funktion
      • Zuweisungsoperator (=) für gesamte Struktur
    • in der Zwischenzeit erlaubt, aber trotzdem:

      • idR. Strukturen dynamisch über Pointer
      • Zuweisung (= Kopieren) selbst schreiben
      • Zuweisung (=) macht sog. shallow copy
    • Shallow copy:

      • nur die oberste Ebene wird kopiert
      • d.h. Werte bei elementaren Variablen
      • d.h. Adressen bei Pointern
      • also: Kopie hat (physisch!) dieselben dynamischen Daten
    • Deep copy:

      • alle Ebenen der Struktur werden kopiert
      • d.h. alle Werte bei elementaren Variablen
      • plus Kopie der dynamischen Inhalte (d.h. durch Pointer adressierter Speicher)

    Strukturen: Speicher allokieren¶

    • Also Funktionen anlegen
      • newStudent: Allokieren und Initialisieren
      • freeStudent: Freigeben des Speichers
      • cloneStudent: Vollständige Kopie der Struktur inkl. dyn. Felder, z.B. Member firstname (sog. deep copy)
      • copyStudent: Kopie der obersten Ebene exkl. dynamischer Felder (sog. shallow copy)
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Student* newStudent() {
      
      // allocate memory for struct variable as before.
      // Do not forget to use sizeof within malloc!
      
      Student* pointer = malloc(sizeof(Student));
      assert( pointer != NULL);
    
      // dereferencing of the pointer yields a struct variable
      // of type Student. The use of brackets is mandatory!
      
      (*pointer).firstname = NULL;
      (*pointer).lastname = NULL;
      (*pointer).studentID = 0;
      (*pointer).studiesID = 0;
      (*pointer).final_test = 0.;
      (*pointer).kreuzerl = 0.;
      (*pointer).exercise = 0.;
    
      return pointer;
    }
    

    Strukturen & Pfeiloperator¶

    • Im Programm ist pointer vom Typ Student*
    • Zugriff auf Members, z.B. (*pointer).firstname
      • Bessere Schreibweise dafür pointer->firstname
    • Strukturen nie statisch, sondern stets dynamisch
      • Verwende gleich student für Typ Student*
    • Funktion newStudent lautet besser wie folgt
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // declaration of structure and corresponding data type
    typedef struct _Student_ {
      char* firstname;
      char* lastname;
      int studentID;
      int studiesID;
      double final_test;
      double kreuzerl;
      double exercise;
    } Student;
    
    // allocate and initialize new student
    Student* newStudent() {
      Student* student = malloc(sizeof(Student));
      assert(student != NULL);
      
      student->firstname = NULL;
      student->lastname = NULL;
      student->studentID = 0;
      student->studiesID = 0;
      student->final_test = 0.;
      student->kreuzerl = 0.;
      student->exercise = 0.;
      
      return student;
    }
    

    Strukturen: Speicher freigeben¶

    • Freigeben einer dynamisch erzeugten Struktur-Variable vom Typ Student
    • Achtung: Zugewiesenen dynamischen Speicher vor Freigabe des Strukturpointers freigeben
      • hier: firstname, lastname
      • alle anderen Members sind elementar
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // free memory allocation
    Student* delStudent(Student* student) {
      assert(student != NULL);
      
      // delete firstname, if memory has been allocated
      if (student->firstname != NULL) {
        free(student->firstname);
      }
        
      // delete lastname, if memory has been allocated
      if (student->lastname != NULL) {
        free(student->lastname);
      }
      
      // delete memory of struct variable and return NULL
      free(student);
      return NULL;
    }
    

    Shallow Copy¶

    • Kopieren einer dynamisch erzeugten Struktur-Variable vom Typ Student
      • Kopieren der obersten Ebene einer Struktur exklusive dynamischen Speicher (Members!)
    • ACHTUNG: Original und Shallow-Kopie teilen sich die dynamischen Felder
      • d.h. physisch derselbe firstname und lastname
      • Änderung firstname vom Original ändert auch firstname der Shallow-Kopie
        • Anwendung von freeStudent auf Kopie und Shallow-Kopie liefert Laufzeitfehler
      • Nach erstem Aufruf ist bereits Speicher von firstname und lastname freigegeben
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // shallow copy of student
    Student* copyStudent(Student* student) {
      Student* copy = newStudent();
      assert(student != NULL);
      
      // note: for pointes, only adresses are copied
      copy->firstname = student->firstname;
      copy->lastname = student->lastname;
      
      // copy of elementary data
      copy->studentID = student->studentID;
      copy->studiesID = student->studiesID;
      copy->final_test = student->final_test;
      copy->kreuzerl = student->kreuzerl;
      copy->exercise = student->exercise;
      
      return copy;
    }
    

    Deep Copy¶

    • Kopieren einer dynamisch erzeugten Struktur-Variable vom Typ Student
    • Vollständige Kopie, inkl. dynamischem Speicher
    • Achtung: Zugewiesenen dynamischen Speicher mitkopieren
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // deep copy of student
    Student* cloneStudent(Student* student) {
      Student* copy = newStudent();
      int length = 0;
      assert( student != NULL);
      
      // full copy of firstname
      if (student->firstname != NULL) {
        length = strlen(student->firstname);
        copy->firstname = malloc((length+1)*sizeof(char));
        assert(copy->firstname != NULL);
        strcpy(copy->firstname, student->firstname);
      }
      
      // full copy of lastname
      if (student->lastname != NULL) {
        length = strlen(student->lastname);
        copy->lastname = malloc((length+1)*sizeof(char));
        assert(copy->lastname != NULL);
        strcpy(copy->lastname, student->lastname);
      }
      
      // copy of elementary data
      copy->studentID = student->studentID;
      copy->studiesID = student->studiesID;
      copy->final_test = student->final_test;
      copy->kreuzerl = student->kreuzerl;
      copy->exercise = student->exercise;
    
      return copy;
    }
    

    Arrays von Strukturen¶

    • Ziel: Array mit Teilnehmern von EPROG erstellen
    • keine statischen Arrays verwenden, sondern dynamische Arrays
      • Studenten-Daten sind vom Typ Student
      • also intern verwaltet mittels Typ Student*
      • also Array vom Typ Student**
    • Zugriff auf Members wie vorher
      • participant[j] ist vom Typ Student*
      • also z.B. participant[j]->firstname
    In [ ]:
    1
    2
    3
    4
    5
    6
    7
    // declare array
    Student** participant = malloc(N*sizeof(Student*));
    
    // allocate memory for participants
    for (j=0; j<N; ++j){
      participant[j] = newStudent();
    }
    

    Schachtelung von Strukturen¶

    • Mitarbeiterdaten strukturieren

      • Name, Wohnadresse, Büroadresse
    • Für employee vom Typ Employee*

      • employee->home Pointer auf Address
      • also z.B. employee->home->city
    • Achtung beim Allokieren, Freigeben, Kopieren
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    typedef struct _Address_ {
      char* street;
      char* number;
      char* city;
      char* zip;
    } Address;
    
    typedef struct _Employee_ {
      char* firstname;
      char* lastname;
      char* title;
      Address* home;
      Address* office;
    } Employee;
    

    Strukturen & Mathematik¶

    Abstand zweier Punkte im R3R3¶

    In [92]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    #include <stdlib.h>
    #include <stdio.h>
    #include <assert.h>
    #include <math.h>
    
    typedef struct _Vector3_ {
      double x;
      double y;
      double z;
    } Vector3;
    
    Vector3* newVector3(double x, double y, double z) {
      Vector3* v = malloc(sizeof(Vector3));
      assert(v != NULL); // ensure proper memory allocation
      v->x = x;
      v->y = y;
      v->z = z;
      return v;
    }
    
    Vector3* delVector3(Vector3* v) {
      assert(v != NULL); // ensure that vector v exists
      free(v);
      return NULL;
    }
    
    double dist(Vector3* v, Vector3* w) {
      assert(v != NULL); // ensure that vector v exists
      assert(w != NULL); // ensure that vector w exists
      return sqrt( (v->x - w->x)*(v->x - w->x)
                   + (v->y - w->y)*(v->y - w->y)
                   + (v->z - w->z)*(v->z - w->z) );
    }
    
    int main() {
      Vector3* v = newVector3(1,1,1);
      Vector3* w = newVector3(1,2,3);
      printf("dist(x,y) = %f\n", dist(v,w));
      v = delVector3(v);
      w = delVector3(w);
    }
    
    dist(x,y) = 2.236068
    

    Strukturen und Vektoren¶

    • Datentyp zur Speicherung von x∈Rnx∈Rn
      • Dimension nn vom Typ int
      • Datenfeld xjxj zur Speicherung von double
    In [93]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #ifndef INCLUDE_VECTOR__
    #define INCLUDE_VECTOR__
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    #include <math.h>
    
    // declaration of new data type Vector
    typedef struct _Vector_ {
      int n;          // Dimension
      double* entry;  // Vector coefficients
    } Vector;
    
    // allocate and initialize new vector of length n
    Vector* newVector(int n);
    
    // free storage of allocated vector and return NULL
    Vector* delVector(Vector* X);
    
    // return length of a vector
    int getVectorN(Vector* X);
    
    // return coefficient Xi of vector X
    double getVectorEntry(Vector* X, int i);
    
    // assign new value to coefficient Xi of vector X
    void setVectorEntry(Vector* X, int i, double Xi);
    
    // some example functions...
    Vector* inputVector();
    double normVector(Vector* X);
    double productVector(Vector* X, Vector* Y);
    
    #endif
    

    Allokieren eines Vektors¶

    • Funktion bekommt Länge n∈Nn∈N des Vektors
    • allokiert Struktur, weist Dimension nn zu
    • allokiert und initialisiert Datenfeld
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Vector* newVector(int n) {
      int i = 0;
      Vector* X = NULL;
      
      assert(n > 0);
      
      X = malloc(sizeof(Vector));
      assert(X != NULL);
      
      X->n = n;
      X->entry = malloc(n*sizeof(double));
      assert(X->entry != NULL);
      
      for (i=0; i<n; ++i) {
        X->entry[i] = 0;
      }
      return X;
    }
    

    Freigeben eines Vektors¶

    • Datenfeld freigeben
    • Struktur freigeben
    • NULL zurückgeben
    In [ ]:
    1
    2
    3
    4
    5
    6
    7
    Vector* delVector(Vector* X) {
      assert(X != NULL);
      free(X->entry);
      free(X);
      
      return NULL;
    }
    

    Zugriff auf Strukturen¶

    • Es ist guter (aber seltener) Programmierstil, auf Members einer Struktur nicht direkt zuzugreifen
    • Stattdessen lieber für jeden Member set und get schreiben
      • Wenn kein set, dann Schreiben nicht erlaubt!
      • Wenn kein get, dann Lesen nicht erlaubt!
    • Dieses Vorgehen erlaubt leichte Umstellung der Datenstruktur bei späteren Modifikationen
    • Dieses Vorgehen vermeidet Inkonsistenzen der Daten und insbesondere Laufzeitfehler
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    int getVectorN(Vector* X) {
      assert(X != NULL);
      return X->n;
    }
    
    double getVectorEntry(Vector* X, int i) {
      assert(X != NULL);
      assert((i >= 0) && (i < X->n));
      return X->entry[i];
    }
    
    void setVectorEntry(Vector* X, int i, double Xi){
      assert(X != NULL);
      assert((i >= 0) && (i < X->n));
      X->entry[i] = Xi;
    }
    

    Beispiel: Vektor einlesen¶

    • Einlesen von n∈Nn∈N und eines Vektors x∈Rnx∈Rn
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    Vector* inputVector() {
      
      Vector* X = NULL;
      int i = 0;
      int n = 0;
      double input = 0;
      
      printf("Length of the vector n=");
      scanf("%d",&n);
      assert(n > 0);
      
      X = newVector(n);
      assert(X != NULL);
      
      for (i=0; i<n; ++i) {
        input = 0;
        printf("x[%d]=",i);
        scanf("%lf",&input);
        setVectorEntry(X,i,input);
      }
      
      return X;
    }
    

    Beispiel: Euklidische Norm¶

    • Berechne ∥x∥:=(n∑j=1x2j)1/2‖x‖:=(∑j=1nxj2)1/2 für x∈Rnx∈Rn
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    double normVector(Vector* X) {
      
      double Xi = 0;
      double norm = 0;
      int n = 0;
      int i = 0;
      
      assert(X != NULL);
     
      n = getVectorN(X);
    
      for (i=0; i<n; ++i) {
        Xi = getVectorEntry(X,i);
        norm = norm + Xi*Xi;
      }
      norm = sqrt(norm);
      
      return norm;
    }
    

    Beispiel: Skalarprodukt¶

    • Berechne x⋅y:=n∑j=1xjyjx⋅y:=∑j=1nxjyj für x,y∈Rnx,y∈Rn
    In [ ]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    double productVector(Vector* X, Vector* Y) {
      
      double Xi = 0;
      double Yi = 0;
      double product = 0;
      int n = 0;
      int i = 0;
    
      assert(X != NULL);
      assert(Y != NULL);
      
      n = getVectorN(X);
      assert(n == getVectorN(Y));
      
      for (i=0; i<n; ++i) {
        Xi = getVectorEntry(X,i);
        Yi = getVectorEntry(Y,i);
        product = product + Xi*Yi;
      }
      
      return product;
    }
    

    Zahldarstellung im Computer¶

    Speichereinheiten¶

    • 1 Bit = 1 b = kleinste Einheit, speichert 00 oder 11
    • 1 Byte = 1 B = Zusammenfassung von 8 Bit
    • 1 Kilobyte = 1 KB = 1024 Byte
      • 1024=2101024=210 nächste 2er Potenz an 10001000
    • 1 Megabyte = 1 MB = 1024 KB
    • 1 Gigabyte = 1 GB = 1024 MB
    • 1 Terabyte = 1 TB = 1024 GB

    Speicherung von Zahlen¶

    • Zur Speicherung von Zahlen wird je nach Datentyp fixe Anzahl an Bytes verwendet
    • Konsequenz:
      • pro Datentyp gibt es nur endlich viele Zahlen
      • d.h. es gibt jeweils größte und kleinste Zahl!

    Ganzzahlen¶

    • Mit nn Bits kann man 2n2n Ganzzahlen darstellen
    • Standardmäßig betrachtet man
      • entweder alle ganzen Zahlen in [0,2n−1][0,2n−1]
      • oder alle ganzen Zahlen in [−2n−1,2n−1−1][−2n−1,2n−1−1]

    Integer-Arithmetik¶

    • exakte Arithmetik innerhalb [intmin,intmax][intmin,intmax]

    • Überlauf: Ergebnis von Rechnung >> intmaxintmax

    • Unterlauf: Ergebnis von Rechnung << intminintmin

    • Integer-Arithmetik ist i.d.R. Modulo-Arithmetik

      • d.h. Zahlenbereich ist geschlossen
        • intmax+1intmax+1 liefert intminintmin
        • intmin−1intmin−1 liefert intmaxintmax
      • nicht im C-Standard festgelegt!

    Beispiel: unsigned 4-bit Integer¶

    • 4 Speicherplätze für 0/1 ⟹⟹ 16 verschiedene Zahlen (0,…,150,…,15)
    • Formel: a_3 a_2 a_1 a_0 =∑3i=0ai2i=∑i=03ai2i mit ai∈{0,1}ai∈{0,1}

      • 0 0 0 0 =0=0
      • 0 0 0 1 =1=1
      • 0 0 1 0 =2=2
      • 0 0 1 1 =3=3
      • ……
      • 1 0 0 0 =8=8
      • ……
      • 1 1 1 1 =15=15
    • Addition wie im 10er System (mit Übertrag falls Ergebnis >1>1)

      • 0 0 0 1 + 0 1 0 1 = 0 1 1 0 (1+5=61+5=6)

    Beispiel: 4-bit Integer¶

    • 4 Speicherplätze für 0/1 ⟹⟹ 16 verschiedene Zahlen (−8,…,0,…,7−8,…,0,…,7)

      • 0 0 0 0 =0=0
      • ……
      • 0 1 1 1 =7=7
    • Beobachtung: 0 1 1 1 + 1 0 0 1 = 0 0 0 0 (+1+1 Übertrag bleibt übrig)

      • daher: 1 0 0 1 =−7=−7
      • 1 0 0 0 =−8=−8
      • ……
      • 1 1 1 1 =−1=−1
    • Daher: 0 1 1 1+0 0 0 1 = 1 0 0 0 (Überlauf 7+1=−87+1=−8)

    Beispiel: Integer in C¶

    In [100]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    #include <stdio.h>
    
    int main() {
      int j = 0;
      int n = 8*sizeof(int); // number bits per int
      int min = 1;
    
      // compute 2^(n-1)
      for (j=1; j<n; ++j) {
        min = 2*min;
      }
      printf("n=%d, min=%d, max=%d\n",n,min,min-1);
    }
    
    n=32, min=-2147483648, max=2147483647
    

    2 Milliarden sind nicht viel!¶

    • Achtung: 13!=13⋅12!>13⋅479⋅106>613!=13⋅12!>13⋅479⋅106>6 Millarden
    In [101]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    #include <stdio.h>
    
    int main() {
      int n = 1;
      int factorial = 1;
    
      do {
        ++n;
        factorial = n*factorial;
        printf("n=%d, n!=%d\n",n,factorial);
      } while (factorial < n*factorial);
    
      printf("n=%d, n!>%d\n",n+1,n*factorial);
    }
    
    n=2, n!=2
    n=3, n!=6
    n=4, n!=24
    n=5, n!=120
    n=6, n!=720
    n=7, n!=5040
    n=8, n!=40320
    n=9, n!=362880
    n=10, n!=3628800
    n=11, n!=39916800
    n=12, n!=479001600
    n=13, n!=1932053504
    n=14, n!>-653108224
    

    Variablentypen short, int, long¶

    • nn Bits →→ 2n2n Ganzzahlen

    • In C sind short, int, long mit Vorzeichen

      • d.h. ganze Zahlen in [−2n−1,2n−1−1][−2n−1,2n−1−1]
    • Ganzzahlen ≥0≥0 durch zusätzliches unsigned

      • d.h. ganze Zahlen in [0,2n−1][0,2n−1]
      • z.B. unsigned int var1 = 0;
    • Es gilt stets short ≤≤ int ≤≤ long ≤≤ long long
      • Standardlängen:
        • 2 Byte (short)
        • 4 Byte (int)
        • 8 Byte (long long, seit C99)
      • In der Regel gilt int = long
    • https://en.wikipedia.org/wiki/C_data_types
    • Für die UE nur int verwenden

    Variablentypen char¶

    • char ist Ganzzahl-Typ, idR. 1 Byte
    • Zeichen sind intern Ganzzahlen zugeordnet

      • idR. ASCII-Code
      • siehe z.B. http://www.asciitable.com/
      • ASCII-Code eines Buchstabens erhält man durch einfache Hochkommata
        • Deklaration char var = 'A'; weist var ASCII-Code des Buchstabens A zu
    • Platzhalter eines Zeichens für printf und scanf

      • %c als Zeichen
      • %d als Ganzzahl
    • ACHTUNG:'A' erzeugt char, "A" erzeugt char*

    In [102]:
    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>
    
    int main() {
      char var = 'A';
    
      printf("sizeof(var) = %d\n", sizeof(var));
      printf("%c %d\n",var,var);
    }
    
    sizeof(var) = 1
    A 65
    

    Gleitkommazahlen¶

    Gleitkommadarstellung 1/3¶

    • SATZ: Zu x∈Rx∈R existieren

      • Vorzeichen σ∈{±1}σ∈{±1}
      • Ziffern ak∈{0,1}ak∈{0,1}
      • Exponent e∈Ze∈Z sodass gilt x=σ(∞∑k=1ak2−k)2ex=σ(∑k=1∞ak2−k)2e
    • Darstellung ist nicht eindeutig, da z.B. 1=∞∑k=12−k1=∑k=1∞2−k

      • geometrische Reihe: ∞∑k=02−k=11−1/2=2∑k=0∞2−k=11−1/2=2
      • z.B. (∞∑k=12−k)20=∞∑k=12−k=1=(∞∑k=22−k)21(∑k=1∞2−k)20=∑k=1∞2−k=1=(∑k=2∞2−k)21

    Bemerkungen zum Dezimalsystem¶

    • Satz gilt für jede Basis b∈N≥2b∈N≥2
      • Ziffern dann aj∈{0,1,…,b−1}aj∈{0,1,…,b−1}
    • Dezimalsystem b=10b=10 ist übliches System
      • 47.11=(4⋅10−1+7⋅10−2+1⋅10−3+1⋅10−4)⋅10247.11=(4⋅10−1+7⋅10−2+1⋅10−3+1⋅10−4)⋅102
        • a1=4a1=4, a2=7a2=7, a3=1a3=1, a4=1a4=1, e=2e=2
    • Nichteindeutigkeit im Dezimalsystem: 0.¯¯¯9=10.9¯=1

    Bemerkungen zum Binärsystem¶

    • Mit b=2b=2 sind gekürzte Brüche genau dann als endliche Summe darstellbar (nicht als unendliche Reihe), wenn der Nenner Zweierpotenz ist:

      • M∑k=1ak2−k∑k=1Mak2−k hat Nenner mit Zweierpotenz
      • Eindeutigkeit der Primfaktorzerlegung
        • formaler Beweis = freiwillige Übung
    • z.B. keine exakte Darstellung für 1/101/10 für b=2b=2

    Definition von Gleitkommazahlensystemen¶

    • Gleitkommazahlsystem F(2,M,emin,emax)⊂QF(2,M,emin,emax)⊂Q

      • endliche Teilmenge!
      • Mantissenlänge M∈NM∈N
      • Exponentialschranken emin<0<emaxemin<0<emax
    • x∈Fx∈F hat Darstellung x=σ(M∑k=1ak2−k)2ex=σ(∑k=1Mak2−k)2e mit

      • Vorzeichen σ∈{±1}σ∈{±1}
      • Ziffern aj∈{0,1}aj∈{0,1} mit a1=1a1=1 oder e=emine=emin
        • normalisierte Gleitkommazahl (a1=1a1=1)
        • denormalisierte Gleitkommazahl (e=emine=emin)
      • Exponent e∈Ze∈Z mit emin≤e≤emaxemin≤e≤emax
    • Darstellung von x∈Fx∈F ist eindeutig (Übung!)

    • Ziffer a1a1 muss nicht gespeichert werden
      • implizites erstes Bit

    Arithmetik für Gleitkommazahlen¶

    • Ergebnis Inf, -Inf bei Überlauf (oder 1./0.)
      • +∞+∞ (Inf), −∞−∞ (-Inf)
    • Ergebnis NaN, falls nicht definiert (z.B. 0./0.)

      • not a number (NaN)
    • Arithmetik ist approximativ, nicht exakt

    • Genauigkeit nimmt ab wenn Zahlen größer werden, d.h., nur relative Fehler sind sinnvoll.

      • Beispiel: F(2,M,−3,3)F(2,M,−3,3)

    Schlechte Kondition¶

    • Eine Aufgabe ist numerisch schlecht gestellt bzw. schlecht konditioniert, falls kleine Änderungen der Daten auf große Änderungen im Ergebnis führen

      • z.B. hat Dreieck mit gegebenen Seitenlängen einen rechten Winkel?
      • z.B. liegt gegebener Punkt auf Kreisrand?
    • Implementierung sinnlos, weil Ergebnis zufällig!

    • bessere Fragestellungen:

      • Hat ein Dreieck mit gegebenen Seitenlängen einen Winkel ≈90∘≈90∘?
      • Liegt ein Punkt in einem εε-Schlauch um den Kreisrand?

    Rechenfehler¶

    • Aufgrund von Rechenfehlern darf man Gleitkommazahlen nie auf Gleichheit überprüfen
      • Statt x=yx=y prüfen, ob Fehler |x−y||x−y| klein ist
      • z.B. |x−y|≤ε⋅max{|x|,|y|}|x−y|≤ε⋅max{|x|,|y|} mit ε=10−13ε=10−13
    In [103]:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>
    #include <math.h>
    
    int main() {
      double x = (116./100.)*100.;
    
      printf("x=%f\n",x);
      printf("floor(x)=%f\n",floor(x));
    
      if (x==116.) {
        printf("There holds x==116\n");
      }
      else {
        printf("x = %.15f\n",x);
      }
    }
    
    x=116.000000
    floor(x)=115.000000
    x = 115.999999999999986
    

    Variablentypen float, double¶

    • float ist idR. einfache Genauigkeit nach IEEE-754-Standard

      • F(2,24,−126,127)F(2,24,−126,127) →→ 4 Byte = 32 Bit
        • 32b = 8b Exp. + 23b Mantisse + 1b Vorz.
      • sog. single precision
      • ca. 7 signifikante Dezimalstellen
    • double ist idR. doppelte Genauigkeit nach IEEE-754-Standard

      • F(2,53,−1022,1023)F(2,53,−1022,1023) →→ 8 Byte = 64 Bit
        • 64b = 11b Exp. + 52b Mantisse + 1b Vorz.
      • sog. double precision
      • ca. 16 signifikante Dezimalstellen
    • Platzhalter in printf: %a.bf gibt a Stellen (inklusive '.') und davon b Nachkommastellen aus

    • %e gibt Zahlen in Exponentialdarstellung aus
    In [104]:
    1
    2
    3
    4
    5
    6
    7
    #include <stdio.h>
    int main() {
      double x = 2./3.;
      float y = 2./3.;
      printf("%17.6f, %e\n", x, x);
      printf("%16.15f, %.15e\n",y, y);
    }
    
             0.666667, 6.666667e-01
    0.666666686534882, 6.666666865348816e-01